[
  {
    "path": ".coveragerc",
    "content": "[run]\nomit =\n    examples/*\n    tests/*\nconcurrency = multiprocessing\nparallel = true\nsigterm = true\ndata_file = .coverage\nsource = fastapi_mcp\n\n[report]\nshow_missing = true\n\n[paths]\nsource =\n    fastapi_mcp/"
  },
  {
    "path": ".cursorignore",
    "content": "# Repomix output\n!repomix-output.txt\n!repomix-output.xml"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: \"[BUG]\"\nlabels: bug\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior, including example code.\n\n**System Info**\nPlease specify the relevant information of your work environment.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/documentation.md",
    "content": "---\nname: Documentation\nabout: Report an issue related to the fastapi-mcp documentation/examples\ntitle: ''\nlabels: documentation\nassignees: ''\n\n---\n\n\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/codecov.yml",
    "content": "coverage:\n  status:\n    project:\n      default:\n        base: pr\n        target: auto\n        threshold: 0.5%\n        informational: false\n        only_pulls: true\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n    open-pull-requests-limit: 10\n    labels:\n      - \"dependencies\"\n      - \"github-actions\"\n\n  - package-ecosystem: \"pip\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n    open-pull-requests-limit: 10\n    labels:\n      - \"dependencies\"\n      - \"python\""
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "## Describe your changes\n\n## Issue ticket number and link (if applicable)\n\n## Screenshots of the feature / bugfix\n\n## Checklist before requesting a review\n- [ ] Added relevant tests\n- [ ] Run ruff & mypy\n- [ ] All tests pass\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n\njobs:\n  ruff:\n    name: Ruff\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Install uv\n        uses: astral-sh/setup-uv@v5\n        with:\n          version: \"0.6.12\"\n          enable-cache: true\n          cache-dependency-glob: \"uv.lock\"\n\n      - name: Set up Python\n        uses: actions/setup-python@v5\n        with:\n          python-version-file: \".python-version\"\n\n      - name: Install dependencies\n        run: uv sync --all-extras --dev\n\n      - name: Lint with Ruff\n        run: uv run ruff check .\n\n  mypy:\n    name: MyPy\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Install uv\n        uses: astral-sh/setup-uv@v5\n        with:\n          version: \"0.6.12\"\n          enable-cache: true\n          cache-dependency-glob: \"uv.lock\"\n\n      - name: Set up Python\n        uses: actions/setup-python@v5\n        with:\n          python-version-file: \".python-version\"\n\n      - name: Install dependencies\n        run: uv sync --all-extras --dev\n\n      - name: Type check with MyPy\n        run: uv run mypy .\n\n  test:\n    name: Test Python ${{ matrix.python-version }}\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        python-version: [\"3.10\", \"3.11\", \"3.12\"]\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Install uv\n        uses: astral-sh/setup-uv@v5\n        with:\n          version: \"0.6.12\"\n          python-version: ${{ matrix.python-version }}\n          enable-cache: true\n          cache-dependency-glob: \"uv.lock\"\n\n      - name: Install dependencies\n        run: uv sync --all-extras --dev\n\n      - name: Run tests\n        run: uv run pytest --cov=fastapi_mcp --cov-report=xml\n\n      - name: Upload coverage to Codecov\n        uses: codecov/codecov-action@v5\n        with:\n          token: ${{ secrets.CODECOV_TOKEN }}\n          fail_ci_if_error: false\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  release:\n    types: [created]\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      - name: Install uv\n        uses: astral-sh/setup-uv@v5\n        with:\n          version: \"0.6.12\"\n          enable-cache: true\n\n      - name: Set up Python\n        uses: actions/setup-python@v5\n        with:\n          python-version-file: \".python-version\"\n\n      - name: Install build dependencies\n        run: |\n          uv sync --all-extras --dev\n          uv pip install build twine\n\n      - name: Build and publish\n        env:\n          TWINE_USERNAME: __token__\n          TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}\n        run: |\n          uv run python -m build\n          uv run twine check dist/*\n          uv run twine upload dist/*\n"
  },
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\ncover/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\n.pybuilder/\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/\n\n# PyCharm\n#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can\n#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore\n#  and can be added to the global gitignore or merged into this file.\n.idea/\n*.iml\n*.iws\n*.ipr\n*.iws\n.idea_modules/\n\n# VSCode\n.vscode/\n\n# Ruff linter\n.ruff_cache/\n\n# Mac/OSX\n.DS_Store\n\n# Windows\nThumbs.db\nehthumbs.db\nDesktop.ini\n\n# Repomix output\nrepomix-output.txt\nrepomix-output.xml"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "repos:\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v5.0.0\n    hooks:\n      - id: trailing-whitespace\n        exclude: \\.(md|mdx)$\n      - id: check-yaml\n      - id: check-added-large-files\n\n  - repo: https://github.com/astral-sh/ruff-pre-commit\n    rev: v0.9.10\n    hooks:\n      - id: ruff\n        args: [--fix]\n      - id: ruff-format\n"
  },
  {
    "path": ".python-version",
    "content": "3.12"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [0.4.0]\n\n🚀 **FastAPI-MCP now supports Streamable HTTP transport.**\n\nHTTP transport is now the recommended approach, following the specification that positions HTTP as the standard while maintaining SSE for backwards compatibility.\n\n### ⚠️ Breaking Changes\n- **`mount()` method is deprecated** and will be removed in a future version. Use `mount_http()` for HTTP transport (recommended) or `mount_sse()` for SSE transport.\n\n### Added\n- 🎉 **Streamable HTTP Transport Support** - New `mount_http()` method implementing the MCP Streamable HTTP specification\n- 🎉 **Stateful Session Management** - For both HTTP and SSE transports\n\n## [0.3.7]\n\n### Fixed\n- 🐛 Fix a bug with OAuth default_scope (#123)\n\n## [0.3.6]\n\nSkipped 0.3.5 due to a broken release attempt.\n\n### Added\n- 🚀 Add configurable HTTP header forwarding (#181)\n\n### Fixed\n- 🐛 Fix a bug with handling FastAPI `root_path` parameter (#163)\n\n## [0.3.4]\n\n### Fixed\n- 🐛 Update the `mcp` dependency to `1.8.1`. Fixes [Issue #134](https://github.com/tadata-org/fastapi_mcp/issues/134) that was caused after a breaking change in mcp sdk 1.8.0.\n\n## [0.3.3]\n\nFixes the broken release from 0.3.2.\n\n### Fixed\n- 🐛 Fix critical bug in openapi conversion (missing `param_desc` definition) (#107, #99)\n- 🐛 Fix non-ascii support (#66)\n\n## [0.3.2] - Broken\n\nThis is a broken release and should not be used.\n\n### Fixed\n- 🐛 Fix a bug preventing simple setup of [basic token passthrough](docs/03_authentication_and_authorization.md#basic-token-passthrough)\n\n## [0.3.1]\n\n🚀 FastApiMCP now supports MCP Authorization!\n\nYou can now add MCP-compliant OAuth configuration in a FastAPI-native way, using your existing FastAPI `Depends()` that we all know and love.\n\n### Added\n- 🎉 Support for Authentication / Authorization compliant to [MCP 2025-03-26 Specification](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization), using OAuth 2.1. (#10)\n- 🎉 Support passing http headers to tool calls (#82)\n\n## [0.3.0]\n\n🚀 FastApiMCP now works with ASGI-transport by default.\n\nThis means the `base_url` argument is redundant, and thus has been removed.\n\nYou can still set up an explicit base URL using the `http_client` argument, and injecting your own `httpx.AsyncClient` if necessary.\n\n### Removed\n- ⚠️ Breaking Change: Removed `base_url` argument since it's not used anymore by the MCP transport.\n\n### Fixed\n- 🐛 Fix short timeout issue (#71), increasing the default timeout to 10\n\n\n## [0.2.0]\n\n### Changed\n- Complete refactor from function-based API to a new class-based API with `FastApiMCP`\n- Explicit separation between MCP instance creation and mounting with `mcp = FastApiMCP(app)` followed by `mcp.mount()`\n- FastAPI-native approach for transport providing more flexible routing options\n- Updated minimum MCP dependency to v1.6.0\n\n### Added\n- Support for deploying MCP servers separately from API service\n- Support for \"refreshing\" with `setup_server()` when dynamically adding FastAPI routes. Fixes [Issue #19](https://github.com/tadata-org/fastapi_mcp/issues/19)\n- Endpoint filtering capabilities through new parameters:\n  - `include_operations`: Expose only specific operations by their operation IDs\n  - `exclude_operations`: Expose all operations except those with specified operation IDs\n  - `include_tags`: Expose only operations with specific tags\n  - `exclude_tags`: Expose all operations except those with specific tags\n\n### Fixed\n- FastAPI-native approach for transport. Fixes [Issue #28](https://github.com/tadata-org/fastapi_mcp/issues/28)\n- Numerous bugs in OpenAPI schema to tool conversion, addressing [Issue #40](https://github.com/tadata-org/fastapi_mcp/issues/40) and [Issue #45](https://github.com/tadata-org/fastapi_mcp/issues/45)\n\n### Removed\n- Function-based API (`add_mcp_server`, `create_mcp_server`, etc.)\n- Custom tool support via `@mcp.tool()` decorator\n\n## [0.1.8]\n\n### Fixed\n- Remove unneeded dependency.\n\n## [0.1.7]\n\n### Fixed\n- [Issue #34](https://github.com/tadata-org/fastapi_mcp/issues/34): Fix syntax error.\n\n## [0.1.6]\n\n### Fixed\n- [Issue #23](https://github.com/tadata-org/fastapi_mcp/issues/23): Hide handle_mcp_connection tool.\n\n## [0.1.5]\n\n### Fixed\n- [Issue #25](https://github.com/tadata-org/fastapi_mcp/issues/25): Dynamically creating tools function so tools are useable.\n\n## [0.1.4]\n\n### Fixed\n- [Issue #8](https://github.com/tadata-org/fastapi_mcp/issues/8): Converted tools unuseable due to wrong passing of arguments.\n\n## [0.1.3]\n\n### Fixed\n- Dependency resolution issue with `mcp` package and `pydantic-settings`\n\n## [0.1.2]\n\n### Changed\n- Complete refactor: transformed from a code generator to a direct integration library\n- Replaced the CLI-based approach with a direct API for adding MCP servers to FastAPI applications\n- Integrated MCP servers now mount directly to FastAPI apps at runtime instead of generating separate code\n- Simplified the API with a single `add_mcp_server` function for quick integration\n- Removed code generation entirely in favor of runtime integration\n\n### Added\n- Main `add_mcp_server` function for simple MCP server integration\n- Support for adding custom MCP tools alongside API-derived tools\n- Improved test suite\n- Manage with uv\n\n### Removed\n- CLI interface and all associated commands (generate, run, install, etc.)\n- Code generation functionality\n\n## [0.1.1] - 2024-07-03\n\n### Fixed\n- Added support for PEP 604 union type syntax (e.g., `str | None`) in FastAPI endpoints\n- Improved type handling in model field generation for newer Python versions (3.10+)\n- Fixed compatibility issues with modern type annotations in path parameters, query parameters, and Pydantic models\n\n## [0.1.0] - 2024-03-08\n\n### Added\n- Initial release of FastAPI-MCP\n- Core functionality for converting FastAPI applications to MCP servers\n- CLI tool for generating, running, and installing MCP servers\n- Automatic discovery of FastAPI endpoints\n- Type-safe conversion from FastAPI endpoints to MCP tools\n- Documentation preservation from FastAPI to MCP\n- Claude integration for easy installation and use\n- API integration that automatically makes HTTP requests to FastAPI endpoints\n- Examples directory with sample FastAPI application\n- Basic test suite"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to FastAPI-MCP\n\nFirst off, thank you for considering contributing to FastAPI-MCP!\n\n## Development Setup\n\n1.  Make sure you have Python 3.10+ installed\n2.  Install [uv](https://docs.astral.sh/uv/getting-started/installation/) package manager\n3.  Fork the repository\n4.  Clone your fork\n\n    ```bash\n    git clone https://github.com/YOUR-USERNAME/fastapi_mcp.git\n    cd fastapi-mcp\n\n    # Add the upstream remote\n    git remote add upstream https://github.com/tadata-org/fastapi_mcp.git\n    ```\n\n5.  Set up the development environment:\n\n    ```bash\n    uv sync\n    ```\n\n    That's it! The `uv sync` command will automatically create and use a virtual environment.\n\n6.  Install pre-commit hooks:\n\n    ```bash\n    uv run pre-commit install\n    uv run pre-commit run\n    ```\n\n    Pre-commit hooks will automatically run checks (like ruff, formatting, etc.) when you make a commit, ensuring your code follows our style guidelines.\n\n### Running Commands\n\nYou have two options for running commands:\n\n1.  **With the virtual environment activated**:\n    ```bash\n    source .venv/bin/activate  # On Windows: .venv\\Scripts\\activate\n\n    # Then run commands directly\n    pytest\n    mypy .\n    ruff check .\n    ```\n\n2.  **Without activating the virtual environment**:\n    ```bash\n    # Use uv run prefix for all commands\n    uv run pytest\n    uv run mypy .\n    uv run ruff check .\n    ```\n\nBoth approaches work - use whichever is more convenient for you.\n\n> **Note:** For simplicity, commands in this guide are mostly written **without** the `uv run` prefix. If you haven't activated your virtual environment, remember to prepend `uv run` to all python-related commands and tools.\n\n### Adding Dependencies\n\nWhen adding new dependencies to the library:\n\n1.  **Runtime dependencies** - packages needed to run the application:\n    ```bash\n    uv add new-package\n    ```\n\n2.  **Development dependencies** - packages needed for development, testing, or CI:\n    ```bash\n    uv add --group dev new-package\n    ```\n\nAfter adding dependencies, make sure to:\n1.  Test that everything works with the new package\n2.  Commit both `pyproject.toml` and `uv.lock` files:\n    ```bash\n    git add pyproject.toml uv.lock\n    git commit -m \"Add new-package dependency\"\n    ```\n\n## Development Process\n\n1. Fork the repository and set the upstream remote\n2. Create a feature branch (`git checkout -b feature/amazing-feature`)\n3. Make your changes\n4. Run type checking (`mypy .`)\n5. Run the tests (`pytest`)\n6. Format your code (`ruff check .` and `ruff format .`). Not needed if pre-commit is installed, as it will run it for you.\n7. Commit your changes (`git commit -m 'Add some amazing feature'`)\n8. Push to the branch (`git push origin feature/amazing-feature`)\n9. Open a Pull Request. Make sure the Pull Request's base branch is [the original repository's](https://github.com/tadata-org/fastapi_mcp/) `main` branch.\n\n## Code Style\n\nWe use the following tools to ensure code quality:\n\n- **ruff** for linting and formatting\n- **mypy** for type checking\n\nPlease make sure your code passes all checks before submitting a pull request:\n\n```bash\n# Check code formatting and style\nruff check .\nruff format .\n\n# Check types\nmypy .\n```\n\n## Testing\n\nWe use pytest for testing. Please write tests for any new features and ensure all tests pass:\n\n```bash\n# Run all tests\npytest\n```\n\n## Pull Request Process\n\n1. Ensure your code follows the style guidelines of the project\n2. Update the README.md with details of changes if applicable\n3. The versioning scheme we use is [SemVer](http://semver.org/)\n4. Include a descriptive commit message\n5. Your pull request will be merged once it's reviewed and approved\n\n## Code of Conduct\n\nPlease note we have a code of conduct, please follow it in all your interactions with the project.\n\n- Be respectful and inclusive\n- Be collaborative\n- When disagreeing, try to understand why\n- A diverse community is a strong community\n\n## Questions?\n\nDon't hesitate to open an issue if you have any questions about contributing to FastAPI-MCP."
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 Tadata Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "include LICENSE\ninclude README.md\ninclude INSTALL.md\ninclude pyproject.toml\ninclude setup.py\n\nrecursive-include examples *.py *.md\nrecursive-include tests *.py\n\nglobal-exclude *.py[cod] __pycache__ *.so *.dylib .DS_Store "
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\"><a href=\"https://github.com/tadata-org/fastapi_mcp\"><img src=\"https://github.com/user-attachments/assets/7e44e98b-a0ba-4aff-a68a-4ffee3a6189c\" alt=\"fastapi-to-mcp\" height=100/></a></p>\n\n<div align=\"center\">\n  <span style=\"font-size: 0.85em; font-weight: normal;\">Built by <a href=\"https://tadata.com\">Tadata</a></span>\n</div>\n\n<h1 align=\"center\">\n  FastAPI-MCP\n</h1>\n\n<div align=\"center\">\n<a href=\"https://trendshift.io/repositories/14064\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/14064\" alt=\"tadata-org%2Ffastapi_mcp | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n</div>\n\n<p align=\"center\">Expose your FastAPI endpoints as Model Context Protocol (MCP) tools, with Auth!</p>\n<div align=\"center\">\n\n[![PyPI version](https://img.shields.io/pypi/v/fastapi-mcp?color=%2334D058&label=pypi%20package)](https://pypi.org/project/fastapi-mcp/)\n[![Python Versions](https://img.shields.io/pypi/pyversions/fastapi-mcp.svg)](https://pypi.org/project/fastapi-mcp/)\n[![FastAPI](https://img.shields.io/badge/FastAPI-009485.svg?logo=fastapi&logoColor=white)](#)\n[![CI](https://github.com/tadata-org/fastapi_mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/tadata-org/fastapi_mcp/actions/workflows/ci.yml)\n[![Coverage](https://codecov.io/gh/tadata-org/fastapi_mcp/branch/main/graph/badge.svg)](https://codecov.io/gh/tadata-org/fastapi_mcp)\n\n</div>\n\n\n<p align=\"center\"><a href=\"https://github.com/tadata-org/fastapi_mcp\"><img src=\"https://github.com/user-attachments/assets/b205adc6-28c0-4e3c-a68b-9c1a80eb7d0c\" alt=\"fastapi-mcp-usage\" height=\"400\"/></a></p>\n\n\n## Features\n\n- **Authentication** built in, using your existing FastAPI dependencies!\n\n- **FastAPI-native:** Not just another OpenAPI -> MCP converter\n\n- **Zero/Minimal configuration** required - just point it at your FastAPI app and it works\n\n- **Preserving schemas** of your request models and response models\n\n- **Preserve documentation** of all your endpoints, just as it is in Swagger\n\n- **Flexible deployment** - Mount your MCP server to the same app, or deploy separately\n\n- **ASGI transport** - Uses FastAPI's ASGI interface directly for efficient communication\n\n\n## Hosted Solution\n\nIf you prefer a managed hosted solution check out [tadata.com](https://tadata.com).\n\n## Installation\n\nWe recommend using [uv](https://docs.astral.sh/uv/), a fast Python package installer:\n\n```bash\nuv add fastapi-mcp\n```\n\nAlternatively, you can install with pip:\n\n```bash\npip install fastapi-mcp\n```\n\n## Basic Usage\n\nThe simplest way to use FastAPI-MCP is to add an MCP server directly to your FastAPI application:\n\n```python\nfrom fastapi import FastAPI\nfrom fastapi_mcp import FastApiMCP\n\napp = FastAPI()\n\nmcp = FastApiMCP(app)\n\n# Mount the MCP server directly to your FastAPI app\nmcp.mount()\n```\n\nThat's it! Your auto-generated MCP server is now available at `https://app.base.url/mcp`.\n\n## Documentation, Examples and Advanced Usage\n\nFastAPI-MCP provides [comprehensive documentation](https://fastapi-mcp.tadata.com/). Additionaly, check out the [examples directory](examples) for code samples demonstrating these features in action.\n\n## FastAPI-first Approach\n\nFastAPI-MCP is designed as a native extension of FastAPI, not just a converter that generates MCP tools from your API. This approach offers several key advantages:\n\n- **Native dependencies**: Secure your MCP endpoints using familiar FastAPI `Depends()` for authentication and authorization\n\n- **ASGI transport**: Communicates directly with your FastAPI app using its ASGI interface, eliminating the need for HTTP calls from the MCP to your API\n\n- **Unified infrastructure**: Your FastAPI app doesn't need to run separately from the MCP server (though [separate deployment](https://fastapi-mcp.tadata.com/advanced/deploy#deploying-separately-from-original-fastapi-app) is also supported)\n\nThis design philosophy ensures minimum friction when adding MCP capabilities to your existing FastAPI services.\n\n\n## Development and Contributing\n\nThank you for considering contributing to FastAPI-MCP! We encourage the community to post Issues and create Pull Requests.\n\nBefore you get started, please see our [Contribution Guide](CONTRIBUTING.md).\n\n## Community\n\nJoin [MCParty Slack community](https://join.slack.com/t/themcparty/shared_invite/zt-30yxr1zdi-2FG~XjBA0xIgYSYuKe7~Xg) to connect with other MCP enthusiasts, ask questions, and share your experiences with FastAPI-MCP.\n\n## Requirements\n\n- Python 3.10+ (Recommended 3.12)\n- uv\n\n## License\n\nMIT License. Copyright (c) 2025 Tadata Inc.\n"
  },
  {
    "path": "README_zh-CN.md",
    "content": "<p align=\"center\"><a href=\"https://github.com/tadata-org/fastapi_mcp\"><img src=\"https://github.com/user-attachments/assets/609d5b8b-37a1-42c4-87e2-f045b60026b1\" alt=\"fastapi-to-mcp\" height=\"100\"/></a></p>\n<h1 align=\"center\">FastAPI-MCP</h1>\n<p align=\"center\">一个零配置工具，用于自动将 FastAPI 端点公开为模型上下文协议（MCP）工具。</p>\n<div align=\"center\">\n\n[![PyPI version](https://badge.fury.io/py/fastapi-mcp.svg)](https://pypi.org/project/fastapi-mcp/)\n[![Python Versions](https://img.shields.io/pypi/pyversions/fastapi-mcp.svg)](https://pypi.org/project/fastapi-mcp/)\n[![FastAPI](https://img.shields.io/badge/FastAPI-009485.svg?logo=fastapi&logoColor=white)](#)\n![](https://badge.mcpx.dev?type=dev 'MCP Dev')\n[![CI](https://github.com/tadata-org/fastapi_mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/tadata-org/fastapi_mcp/actions/workflows/ci.yml)\n[![codecov](https://codecov.io/gh/tadata-org/fastapi_mcp/branch/main/graph/badge.svg)](https://codecov.io/gh/tadata-org/fastapi_mcp)\n\n</div>\n\n<p align=\"center\"><a href=\"https://github.com/tadata-org/fastapi_mcp\"><img src=\"https://github.com/user-attachments/assets/1cba1bf2-2fa4-46c7-93ac-1e9bb1a95257\" alt=\"fastapi-mcp-usage\" height=\"400\"/></a></p>\n\n> 注意：最新版本请参阅 [README.md](README.md).\n\n## 特点\n\n- **直接集成** - 直接将 MCP 服务器挂载到您的 FastAPI 应用\n- **零配置** - 只需指向您的 FastAPI 应用即可工作\n- **自动发现** - 所有 FastAPI 端点并转换为 MCP 工具\n- **保留模式** - 保留您的请求模型和响应模型的模式\n- **保留文档** - 保留所有端点的文档，就像在 Swagger 中一样\n- **灵活部署** - 将 MCP 服务器挂载到同一应用，或单独部署\n- **ASGI 传输** - 默认使用 FastAPI 的 ASGI 接口直接通信，提高效率\n\n## 安装\n\n我们推荐使用 [uv](https://docs.astral.sh/uv/)，一个快速的 Python 包安装器：\n\n```bash\nuv add fastapi-mcp\n```\n\n或者，您可以使用 pip 安装：\n\n```bash\npip install fastapi-mcp\n```\n\n## 基本用法\n\n使用 FastAPI-MCP 的最简单方法是直接将 MCP 服务器添加到您的 FastAPI 应用中：\n\n```python\nfrom fastapi import FastAPI\nfrom fastapi_mcp import FastApiMCP\n\napp = FastAPI()\n\nmcp = FastApiMCP(app)\n\n# 直接将 MCP 服务器挂载到您的 FastAPI 应用\nmcp.mount()\n```\n\n就是这样！您的自动生成的 MCP 服务器现在可以在 `https://app.base.url/mcp` 访问。\n\n## 工具命名\n\nFastAPI-MCP 使用 FastAPI 路由中的`operation_id`作为 MCP 工具的名称。如果您不指定`operation_id`，FastAPI 会自动生成一个，但这些名称可能比较晦涩。\n\n比较以下两个端点定义：\n\n```python\n# 自动生成的 operation_id（类似于 \"read_user_users__user_id__get\"）\n@app.get(\"/users/{user_id}\")\nasync def read_user(user_id: int):\n    return {\"user_id\": user_id}\n\n# 显式 operation_id（工具将被命名为 \"get_user_info\"）\n@app.get(\"/users/{user_id}\", operation_id=\"get_user_info\")\nasync def read_user(user_id: int):\n    return {\"user_id\": user_id}\n```\n\n为了获得更清晰、更直观的工具名称，我们建议在 FastAPI 路由定义中添加显式的`operation_id`参数。\n\n要了解更多信息，请阅读 FastAPI 官方文档中关于 [路径操作的高级配置](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/) 的部分。\n\n## 高级用法\n\nFastAPI-MCP 提供了多种方式来自定义和控制 MCP 服务器的创建和配置。以下是一些高级用法模式：\n\n### 自定义模式描述\n\n```python\nfrom fastapi import FastAPI\nfrom fastapi_mcp import FastApiMCP\n\napp = FastAPI()\n\nmcp = FastApiMCP(\n    app,\n    name=\"我的 API MCP\",\n    describe_all_responses=True,     # 在工具描述中包含所有可能的响应模式\n    describe_full_response_schema=True  # 在工具描述中包含完整的 JSON 模式\n)\n\nmcp.mount()\n```\n\n### 自定义公开的端点\n\n您可以使用 Open API 操作 ID 或标签来控制哪些 FastAPI 端点暴露为 MCP 工具：\n\n```python\nfrom fastapi import FastAPI\nfrom fastapi_mcp import FastApiMCP\n\napp = FastAPI()\n\n# 仅包含特定操作\nmcp = FastApiMCP(\n    app,\n    include_operations=[\"get_user\", \"create_user\"]\n)\n\n# 排除特定操作\nmcp = FastApiMCP(\n    app,\n    exclude_operations=[\"delete_user\"]\n)\n\n# 仅包含具有特定标签的操作\nmcp = FastApiMCP(\n    app,\n    include_tags=[\"users\", \"public\"]\n)\n\n# 排除具有特定标签的操作\nmcp = FastApiMCP(\n    app,\n    exclude_tags=[\"admin\", \"internal\"]\n)\n\n# 结合操作 ID 和标签（包含模式）\nmcp = FastApiMCP(\n    app,\n    include_operations=[\"user_login\"],\n    include_tags=[\"public\"]\n)\n\nmcp.mount()\n```\n\n关于过滤的注意事项：\n- 您不能同时使用`include_operations`和`exclude_operations`\n- 您不能同时使用`include_tags`和`exclude_tags`\n- 您可以将操作过滤与标签过滤结合使用（例如，使用`include_operations`和`include_tags`）\n- 当结合过滤器时，将采取贪婪方法。匹配任一标准的端点都将被包含\n\n### 与原始 FastAPI 应用分开部署\n\n您不限于在创建 MCP 的同一个 FastAPI 应用上提供 MCP 服务。\n\n您可以从一个 FastAPI 应用创建 MCP 服务器，并将其挂载到另一个应用上：\n\n```python\nfrom fastapi import FastAPI\nfrom fastapi_mcp import FastApiMCP\n\n# 您的 API 应用\napi_app = FastAPI()\n# ... 在 api_app 上定义您的 API 端点 ...\n\n# 一个单独的 MCP 服务器应用\nmcp_app = FastAPI()\n\n# 从 API 应用创建 MCP 服务器\nmcp = FastApiMCP(api_app)\n\n# 将 MCP 服务器挂载到单独的应用\nmcp.mount(mcp_app)\n\n# 现在您可以分别运行两个应用：\n# uvicorn main:api_app --host api-host --port 8001\n# uvicorn main:mcp_app --host mcp-host --port 8000\n```\n\n### 在 MCP 服务器创建后添加端点\n\n如果您在创建 MCP 服务器后向 FastAPI 应用添加端点，您需要刷新服务器以包含它们：\n\n```python\nfrom fastapi import FastAPI\nfrom fastapi_mcp import FastApiMCP\n\napp = FastAPI()\n# ... 定义初始端点 ...\n\n# 创建 MCP 服务器\nmcp = FastApiMCP(app)\nmcp.mount()\n\n# 在 MCP 服务器创建后添加新端点\n@app.get(\"/new/endpoint/\", operation_id=\"new_endpoint\")\nasync def new_endpoint():\n    return {\"message\": \"Hello, world!\"}\n\n# 刷新 MCP 服务器以包含新端点\nmcp.setup_server()\n```\n\n### 与 FastAPI 应用的通信\n\nFastAPI-MCP 默认使用 ASGI 传输，这意味着它直接与您的 FastAPI 应用通信，而不需要发送 HTTP 请求。这样更高效，也不需要基础 URL。\n\n如果您需要指定自定义基础 URL 或使用不同的传输方法，您可以提供自己的 `httpx.AsyncClient`：\n\n```python\nimport httpx\nfrom fastapi import FastAPI\nfrom fastapi_mcp import FastApiMCP\n\napp = FastAPI()\n\n# 使用带有特定基础 URL 的自定义 HTTP 客户端\ncustom_client = httpx.AsyncClient(\n    base_url=\"https://api.example.com\",\n    timeout=30.0\n)\n\nmcp = FastApiMCP(\n    app,\n    http_client=custom_client\n)\n\nmcp.mount()\n```\n\n## 示例\n\n请参阅 [examples](examples) 目录以获取完整示例。\n\n## 使用 SSE 连接到 MCP 服务器\n\n一旦您的集成了 MCP 的 FastAPI 应用运行，您可以使用任何支持 SSE 的 MCP 客户端连接到它，例如 Cursor：\n\n1. 运行您的应用。\n\n2. 在 Cursor -> 设置 -> MCP 中，使用您的 MCP 服务器端点的URL（例如，`http://localhost:8000/mcp`）作为 sse。\n\n3. Cursor 将自动发现所有可用的工具和资源。\n\n## 使用 [mcp-proxy stdio](https://github.com/sparfenyuk/mcp-proxy?tab=readme-ov-file#1-stdio-to-sse) 连接到 MCP 服务器\n\n如果您的 MCP 客户端不支持 SSE，例如 Claude Desktop：\n\n1. 运行您的应用。\n\n2. 安装 [mcp-proxy](https://github.com/sparfenyuk/mcp-proxy?tab=readme-ov-file#installing-via-pypi)，例如：`uv tool install mcp-proxy`。\n\n3. 在 Claude Desktop 的 MCP 配置文件（`claude_desktop_config.json`）中添加：\n\n在 Windows 上：\n```json\n{\n  \"mcpServers\": {\n    \"my-api-mcp-proxy\": {\n        \"command\": \"mcp-proxy\",\n        \"args\": [\"http://127.0.0.1:8000/mcp\"]\n    }\n  }\n}\n```\n在 MacOS 上：\n```json\n{\n  \"mcpServers\": {\n    \"my-api-mcp-proxy\": {\n        \"command\": \"/Full/Path/To/Your/Executable/mcp-proxy\",\n        \"args\": [\"http://127.0.0.1:8000/mcp\"]\n    }\n  }\n}\n```\n通过在终端运行`which mcp-proxy`来找到 mcp-proxy 的路径。\n\n4. Claude Desktop 将自动发现所有可用的工具和资源\n\n## 开发和贡献\n\n感谢您考虑为 FastAPI-MCP 做出贡献！我们鼓励社区发布问题和拉取请求。\n\n在开始之前，请参阅我们的 [贡献指南](CONTRIBUTING.md)。\n\n## 社区\n\n加入 [MCParty Slack 社区](https://join.slack.com/t/themcparty/shared_invite/zt-30yxr1zdi-2FG~XjBA0xIgYSYuKe7~Xg)，与其他 MCP 爱好者联系，提问，并分享您使用 FastAPI-MCP 的经验。\n\n## 要求\n\n- Python 3.10+（推荐3.12）\n- uv\n\n## 许可证\n\nMIT License. Copyright (c) 2024 Tadata Inc.\n"
  },
  {
    "path": "docs/advanced/asgi.mdx",
    "content": "---\ntitle: Transport\ndescription: How to communicate with the FastAPI app\nicon: microchip\n---\n\nFastAPI-MCP uses ASGI transport by default, which means it communicates directly with your FastAPI app without making HTTP requests. This is more efficient and doesn't require a base URL.\n\nIt's not even necessary that the FastAPI server will run.\n\nIf you need to specify a custom base URL or use a different transport method, you can provide your own `httpx.AsyncClient`:\n\n```python {7-10, 14}\nimport httpx\nfrom fastapi import FastAPI\nfrom fastapi_mcp import FastApiMCP\n\napp = FastAPI()\n\ncustom_client = httpx.AsyncClient(\n    base_url=\"https://api.example.com\",\n    timeout=30.0\n)\n\nmcp = FastApiMCP(\n    app,\n    http_client=custom_client\n)\n\nmcp.mount()\n```\n"
  },
  {
    "path": "docs/advanced/auth.mdx",
    "content": "---\ntitle: Authentication & Authorization\nicon: key\n---\n\nFastAPI-MCP supports authentication and authorization using your existing FastAPI dependencies.\n\nIt also supports the full OAuth 2 flow, compliant with [MCP Spec 2025-03-26](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization).\n\nIt's worth noting that most MCP clients currently do not support the latest MCP spec, so for our examples we might use a bridge client such as `npx mcp-remote`. We recommend you use it as well, and we'll show our examples using it.\n\n## Basic Token Passthrough\n\nIf you just want to be able to pass a valid authorization header, without supporting a full authentication flow, you don't need to do anything special.\n\nYou just need to make sure your MCP client is sending it:\n\n```json {8-9, 13}\n{\n  \"mcpServers\": {\n    \"remote-example\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"mcp-remote\",\n        \"http://localhost:8000/mcp\",\n        \"--header\",\n        \"Authorization:${AUTH_HEADER}\"\n      ]\n    },\n    \"env\": {\n      \"AUTH_HEADER\": \"Bearer <your-token>\"\n    }\n  }\n}\n```\n\nThis is enough to pass the authorization header to your FastAPI endpoints.\n\nOptionally, if you want your MCP server to reject requests without an authorization header, you can add a dependency:\n\n```python {1-2, 7-9}\nfrom fastapi import Depends\nfrom fastapi_mcp import FastApiMCP, AuthConfig\n\nmcp = FastApiMCP(\n    app,\n    name=\"Protected MCP\",\n    auth_config=AuthConfig(\n        dependencies=[Depends(verify_auth)],\n    ),\n)\nmcp.mount_http()\n```\n\nFor a complete working example of authorization header, check out the [Token Passthrough Example](https://github.com/tadata-org/fastapi_mcp/blob/main/examples/08_auth_example_token_passthrough.py) in the examples folder.\n\n## OAuth Flow\n\nFastAPI-MCP supports the full OAuth 2 flow, compliant with [MCP Spec 2025-03-26](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization).\n\nIt would look something like this:\n\n```python {7-16}\nfrom fastapi import Depends\nfrom fastapi_mcp import FastApiMCP, AuthConfig\n\nmcp = FastApiMCP(\n    app,\n    name=\"MCP With OAuth\",\n    auth_config=AuthConfig(\n        issuer=f\"https://auth.example.com/\",\n        authorize_url=f\"https://auth.example.com/authorize\",\n        oauth_metadata_url=f\"https://auth.example.com/.well-known/oauth-authorization-server\",\n        audience=\"my-audience\",\n        client_id=\"my-client-id\",\n        client_secret=\"my-client-secret\",\n        dependencies=[Depends(verify_auth)],\n        setup_proxies=True,\n    ),\n)\n\nmcp.mount_http()\n```\n\nAnd you can call it like:\n\n```json\n{\n  \"mcpServers\": {\n    \"fastapi-mcp\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"mcp-remote\",\n        \"http://localhost:8000/mcp\",\n        \"8080\"  // Optional port number. Necessary if you want your OAuth to work and you don't have dynamic client registration.\n      ]\n    }\n  }\n}\n```\n\nYou can use it with any OAuth provider that supports the OAuth 2 spec. See explanation on [AuthConfig](#authconfig-explained) for more details.\n\n## Custom OAuth Metadata\n\nIf you already have a properly configured OAuth server that works with MCP clients, or if you want full control over the metadata, you can provide your own OAuth metadata directly:\n\n```python {9, 22}\nfrom fastapi import Depends\nfrom fastapi_mcp import FastApiMCP, AuthConfig\n\nmcp = FastApiMCP(\n    app,\n    name=\"MCP With Custom OAuth\",\n    auth_config=AuthConfig(\n        # Provide your own complete OAuth metadata\n        custom_oauth_metadata={\n            \"issuer\": \"https://auth.example.com\",\n            \"authorization_endpoint\": \"https://auth.example.com/authorize\",\n            \"token_endpoint\": \"https://auth.example.com/token\",\n            \"registration_endpoint\": \"https://auth.example.com/register\",\n            \"scopes_supported\": [\"openid\", \"profile\", \"email\"],\n            \"response_types_supported\": [\"code\"],\n            \"grant_types_supported\": [\"authorization_code\"],\n            \"token_endpoint_auth_methods_supported\": [\"none\"],\n            \"code_challenge_methods_supported\": [\"S256\"]\n        },\n\n        # Your auth checking dependency\n        dependencies=[Depends(verify_auth)],\n    ),\n)\n\nmcp.mount_http()\n```\n\nThis approach gives you complete control over the OAuth metadata and is useful when:\n- You have a fully MCP-compliant OAuth server already configured\n- You need to customize the OAuth flow beyond what the proxy approach offers\n- You're using a custom or specialized OAuth implementation\n\nFor this to work, you have to make sure mcp-remote is running [on a fixed port](#add-a-fixed-port-to-mcp-remote), for example `8080`, and then configure the callback URL to `http://127.0.0.1:8080/oauth/callback` in your OAuth provider.\n\n## Working Example with Auth0\n\nFor a complete working example of OAuth integration with Auth0, check out the [Auth0 Example](https://github.com/tadata-org/fastapi_mcp/blob/main/examples/09_auth_example_auth0.py) in the examples folder. This example demonstrates the simple case of using Auth0 as an OAuth provider, with a working example of the OAuth flow.\n\nFor it to work, you need an .env file in the root of the project with the following variables:\n\n```\nAUTH0_DOMAIN=your-tenant.auth0.com\nAUTH0_AUDIENCE=https://your-tenant.auth0.com/api/v2/\nAUTH0_CLIENT_ID=your-client-id\nAUTH0_CLIENT_SECRET=your-client-secret\n```\n\nYou also need to make sure to configure callback URLs properly in your Auth0 dashboard.\n\n## AuthConfig Explained\n\n### `setup_proxies=True`\n\nMost OAuth providers need some adaptation to work with MCP clients. This is where `setup_proxies=True` comes in - it creates proxy endpoints that make your OAuth provider compatible with MCP clients:\n\n```python\nmcp = FastApiMCP(\n    app,\n    auth_config=AuthConfig(\n        # Your OAuth provider information\n        issuer=\"https://auth.example.com\",\n        authorize_url=\"https://auth.example.com/authorize\",\n        oauth_metadata_url=\"https://auth.example.com/.well-known/oauth-authorization-server\",\n\n        # Credentials registered with your OAuth provider\n        client_id=\"your-client-id\",\n        client_secret=\"your-client-secret\",\n\n        # Recommended, since some clients don't specify them\n        audience=\"your-api-audience\",\n        default_scope=\"openid profile email\",\n\n        # Your auth checking dependency\n        dependencies=[Depends(verify_auth)],\n\n        # Create compatibility proxies - usually needed!\n        setup_proxies=True,\n    ),\n)\n```\n\nYou also need to make sure to configure callback URLs properly in your OAuth provider. With mcp-remote for example, you have to [use a fixed port](#add-a-fixed-port-to-mcp-remote).\n\n### Why Use Proxies?\n\nProxies solve several problems:\n\n1.  **Missing registration endpoints**:  \n    The MCP spec expects OAuth providers to support [dynamic client registration (RFC 7591)](https://datatracker.ietf.org/doc/html/rfc7591), but many don't.\n    Furthermore, dynamic client registration is probably overkill for most use cases.\n    The `setup_fake_dynamic_registration` option (True by default) creates a compatible endpoint that just returns a static client ID and secret.\n\n2.  **Scope handling**:  \n    Some MCP clients don't properly request scopes, so our proxy adds the necessary scopes for you.\n\n3.  **Audience requirements**:  \n    Some OAuth providers require an audience parameter that MCP clients don't always provide. The proxy adds this automatically.\n\n### Add a fixed port to mcp-remote\n\n```json\n{\n  \"mcpServers\": {\n    \"example\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"mcp-remote\",\n        \"http://localhost:8000/mcp\",\n        \"8080\"\n      ]\n    }\n  }\n}\n```\n\nNormally, mcp-remote will start on a random port, making it impossible to configure the OAuth provider's callback URL properly.\n\nYou have to make sure mcp-remote is running on a fixed port, for example `8080`, and then configure the callback URL to `http://127.0.0.1:8080/oauth/callback` in your OAuth provider.\n"
  },
  {
    "path": "docs/advanced/deploy.mdx",
    "content": "---\ntitle: Deploying the Server\nicon: play\n---\n\n## Deploying separately from original FastAPI app\n\nYou are not limited to serving the MCP on the same FastAPI app from which it was created.\n\nYou can create an MCP server from one FastAPI app, and mount it to a different app:\n\n```python {9, 15, }\nfrom fastapi import FastAPI\nfrom fastapi_mcp import FastApiMCP\n\n# Your API app\napi_app = FastAPI()\n# ... define your API endpoints on api_app ...\n\n# A separate app for the MCP server\nmcp_app = FastAPI()\n\n# Create MCP server from the API app\nmcp = FastApiMCP(api_app)\n\n# Mount the MCP server to the separate app\nmcp.mount_http(mcp_app)\n```\n\nThen, you can run both apps separately:\n\n```bash\nuvicorn main:api_app --host api-host --port 8001\nuvicorn main:mcp_app --host mcp-host --port 8000\n```"
  },
  {
    "path": "docs/advanced/refresh.mdx",
    "content": "---\ntitle: Refreshing the Server\ndescription: Adding endpoints after MCP server creation\nicon: arrows-rotate\n---\n\nIf you add endpoints to your FastAPI app after creating the MCP server, you'll need to refresh the server to include them:\n\n```python {9-12, 15}\nfrom fastapi import FastAPI\nfrom fastapi_mcp import FastApiMCP\n\napp = FastAPI()\n\nmcp = FastApiMCP(app)\nmcp.mount_http()\n\n# Add new endpoints after MCP server creation\n@app.get(\"/new/endpoint/\", operation_id=\"new_endpoint\")\nasync def new_endpoint():\n    return {\"message\": \"Hello, world!\"}\n\n# Refresh the MCP server to include the new endpoint\nmcp.setup_server()\n```"
  },
  {
    "path": "docs/advanced/transport.mdx",
    "content": "---\ntitle: MCP Transport\ndescription: Understanding MCP transport methods and how to choose between them\nicon: car\n---\n\nFastAPI-MCP supports two MCP transport methods for client-server communication: **HTTP transport** (recommended) and **SSE transport** (backwards compatibility).\n\n## HTTP Transport (Recommended)\n\nHTTP transport is the **recommended** transport method as it implements the latest MCP Streamable HTTP specification. It provides better session management, more robust connection handling, and aligns with standard HTTP practices.\n\n### Using HTTP Transport\n\n```python {7}\nfrom fastapi import FastAPI\nfrom fastapi_mcp import FastApiMCP\n\napp = FastAPI()\nmcp = FastApiMCP(app)\n\n# Mount using HTTP transport (recommended)\nmcp.mount_http()\n```\n\n## SSE Transport (Backwards Compatibility)\n\nSSE (Server-Sent Events) transport is maintained for backwards compatibility with older MCP implementations.\n\n### Using SSE Transport\n\n```python {7}\nfrom fastapi import FastAPI\nfrom fastapi_mcp import FastApiMCP\n\napp = FastAPI()\nmcp = FastApiMCP(app)\n\n# Mount using SSE transport (backwards compatibility)\nmcp.mount_sse()\n```\n\n## Advanced Configuration\n\nBoth transport methods support the same FastAPI integration features like custom routing and authentication:\n\n```python\nfrom fastapi import FastAPI, APIRouter\nfrom fastapi_mcp import FastApiMCP\n\napp = FastAPI()\nrouter = APIRouter(prefix=\"/api/v1\")\n\nmcp = FastApiMCP(app)\n\n# Mount to custom path with HTTP transport\nmcp.mount_http(router, mount_path=\"/my-http\")\n\n# Or with SSE transport\nmcp.mount_sse(router, mount_path=\"/my-sse\")\n```\n\n## Client Connection Examples\n\n### HTTP Transport Client Connection\n\nFor HTTP transport, MCP clients connect directly to the HTTP endpoint:\n\n```json\n{\n  \"mcpServers\": {\n    \"fastapi-mcp\": {\n      \"url\": \"http://localhost:8000/mcp\"\n    }\n  }\n}\n```\n\n### SSE Transport Client Connection\n\nFor SSE transport, MCP clients use the same URL but communicate via Server-Sent Events:\n\n```json\n{\n  \"mcpServers\": {\n    \"fastapi-mcp\": {\n      \"url\": \"http://localhost:8000/sse\"\n    }\n  }\n}\n```\n"
  },
  {
    "path": "docs/configurations/customization.mdx",
    "content": "---\ntitle: Customization\nicon: pen\n---\n\n## Server metadata\n\nYou can define the MCP server name and description by modifying:\n\n```python {8-9}\nfrom fastapi import FastAPI\nfrom fastapi_mcp import FastApiMCP\n\napp = FastAPI()\n\nmcp = FastApiMCP(\n    app,\n    name=\"My API MCP\",\n    description=\"Very cool MCP server\",\n)\nmcp.mount_http()\n```\n\n## Tool and schema descriptions\n\nWhen creating the MCP server you can include all possible response schemas in tool descriptions by changing the flag `describe_all_responses`, or include full JSON schema in tool descriptions by changing `describe_full_response_schema`:\n\n```python {10-11}\nfrom fastapi import FastAPI\nfrom fastapi_mcp import FastApiMCP\n\napp = FastAPI()\n\nmcp = FastApiMCP(\n    app,\n    name=\"My API MCP\",\n    description=\"Very cool MCP server\",\n    describe_all_responses=True,\n    describe_full_response_schema=True\n)\n\nmcp.mount_http()\n```\n\n## Customizing Exposed Endpoints\n\nYou can control which FastAPI endpoints are exposed as MCP tools using Open API operation IDs or tags to:\n- Only include specific operations\n- Exclude specific operations\n- Only include operations with specific tags\n- Exclude operations with specific tags\n- Combine operation IDs and tags\n\n### Code samples\n\nThe relevant arguments for these configurations are `include_operations`, `exclude_operations`, `include_tags`, `exclude_tags` and can be used as follows:\n\n<CodeGroup>\n    ```python Include Operations {8}\n    from fastapi import FastAPI\n    from fastapi_mcp import FastApiMCP\n\n    app = FastAPI()\n\n    mcp = FastApiMCP(\n        app,\n        include_operations=[\"get_user\", \"create_user\"]\n    )\n    mcp.mount_http()\n    ```\n\n    ```python Exclude Operations {8}\n    from fastapi import FastAPI\n    from fastapi_mcp import FastApiMCP\n\n    app = FastAPI()\n\n    mcp = FastApiMCP(\n        app,\n        exclude_operations=[\"delete_user\"]\n    )\n    mcp.mount_http()\n    ```\n\n    ```python Include Tags {8}\n    from fastapi import FastAPI\n    from fastapi_mcp import FastApiMCP\n\n    app = FastAPI()\n\n    mcp = FastApiMCP(\n        app,\n        include_tags=[\"users\", \"public\"]\n    )\n    mcp.mount_http()\n    ```\n\n    ```python Exclude Tags {8}\n    from fastapi import FastAPI\n    from fastapi_mcp import FastApiMCP\n\n    app = FastAPI()\n\n    mcp = FastApiMCP(\n        app,\n        exclude_tags=[\"admin\", \"internal\"]\n    )\n    mcp.mount_http()\n    ```\n\n    ```python Combined (include mode) {8-9}\n    from fastapi import FastAPI\n    from fastapi_mcp import FastApiMCP\n\n    app = FastAPI()\n\n    mcp = FastApiMCP(\n        app,\n        include_operations=[\"user_login\"],\n        include_tags=[\"public\"]\n    )\n    mcp.mount_http()\n    ```\n</CodeGroup>\n\n### Notes on filtering\n\n- You cannot use both `include_operations` and `exclude_operations` at the same time\n- You cannot use both `include_tags` and `exclude_tags` at the same time\n- You can combine operation filtering with tag filtering (e.g., use `include_operations` with `include_tags`)\n- When combining filters, a greedy approach will be taken. Endpoints matching either criteria will be included"
  },
  {
    "path": "docs/configurations/tool-naming.mdx",
    "content": "---\ntitle: Tool Naming\nicon: input-text    \n---\n\nFastAPI-MCP uses the `operation_id` from your FastAPI routes as the MCP tool names. When you don't specify an `operation_id`, FastAPI auto-generates one, but these can be cryptic.\n\nCompare these two endpoint definitions:\n\n```python {2, 7}\n# Auto-generated operation_id (something like \"read_user_users__user_id__get\")\n@app.get(\"/users/{user_id}\")\nasync def read_user(user_id: int):\n    return {\"user_id\": user_id}\n\n# Explicit operation_id (tool will be named \"get_user_info\")\n@app.get(\"/users/{user_id}\", operation_id=\"get_user_info\")\nasync def read_user(user_id: int):\n    return {\"user_id\": user_id}\n```\n\nFor clearer, more intuitive tool names, we recommend adding explicit `operation_id` parameters to your FastAPI route definitions.\n\nTo find out more, read FastAPI's official docs about [advanced config of path operations.](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/)\n"
  },
  {
    "path": "docs/docs.json",
    "content": "{\n  \"$schema\": \"https://mintlify.com/docs.json\",\n  \"name\": \"FastAPI MCP\",\n  \"background\": {\n    \"color\": {\n      \"dark\": \"#222831\",\n      \"light\": \"#EEEEEE\"\n    },\n    \"decoration\": \"windows\"\n  },\n  \"colors\": {\n    \"primary\": \"#6d45dc\",\n    \"light\": \"#9f8ded\",\n    \"dark\": \"#6a42d7\"\n  },\n  \"description\": \"Convert your FastAPI app into a MCP server with zero configuration\",\n  \"favicon\": \"media/favicon.png\",\n  \"navigation\": {\n    \"groups\": [\n      {\n        \"group\": \"Getting Started\",\n        \"pages\": [\n          \"getting-started/welcome\",\n          \"getting-started/installation\",\n          \"getting-started/quickstart\",\n          \"getting-started/FAQ\",\n          \"getting-started/best-practices\"\n        ]\n      },\n      {\n        \"group\": \"Configurations\",\n        \"pages\": [\"configurations/tool-naming\", \"configurations/customization\"]\n      },\n      {\n        \"group\": \"Advanced Usage\",\n        \"pages\": [\n          \"advanced/auth\",\n          \"advanced/deploy\",\n          \"advanced/refresh\",\n          \"advanced/asgi\",\n          \"advanced/transport\"\n        ]\n      }\n    ],\n    \"global\": {\n      \"anchors\": [\n        {\n          \"anchor\": \"Documentation\",\n          \"href\": \"https://fastapi-mcp.tadata.com/\",\n          \"icon\": \"book-open-cover\"\n        },\n        {\n          \"anchor\": \"Community\",\n          \"href\": \"https://join.slack.com/t/themcparty/shared_invite/zt-30yxr1zdi-2FG~XjBA0xIgYSYuKe7~Xg\",\n          \"icon\": \"slack\"\n        },\n        {\n          \"anchor\": \"Blog\",\n          \"href\": \"https://medium.com/@miki_45906\",\n          \"icon\": \"newspaper\"\n        }\n      ]\n    }\n  },\n  \"logo\": {\n    \"light\": \"/media/dark_logo.png\",\n    \"dark\": \"/media/light_logo.png\",\n    \"href\": \"https://tadata.com/\"\n  },\n  \"navbar\": {\n    \"primary\": {\n      \"href\": \"https://github.com/tadata-org/fastapi_mcp\",\n      \"type\": \"github\"\n    }\n  },\n  \"footer\": {\n    \"socials\": {\n      \"x\": \"https://x.com/makhlevich\",\n      \"github\": \"https://github.com/tadata-org/fastapi_mcp\",\n      \"website\": \"https://tadata.com/\"\n    }\n  },\n  \"theme\": \"mint\"\n}\n"
  },
  {
    "path": "docs/getting-started/FAQ.mdx",
    "content": "---\ntitle: FAQ\ndescription: Frequently Asked Questions\nicon: question\n---\n\n## Usage\n### How do I configure HTTP request timeouts?\nBy default, HTTP requests timeout after 5 seconds. If you have API endpoints that take longer to respond, you can configure a custom timeout by injecting your own httpx client. \n\nFor a working example, see [Configure HTTP Timeout Example](https://github.com/tadata-org/fastapi_mcp/blob/main/examples/07_configure_http_timeout_example.py).\n\n### Why are my tools not showing up in the MCP inspector?\nIf you add endpoints after creating and mounting the MCP server, they won't be automatically registered as tools. You need to either:\n1. Move the MCP creation after all your endpoint definitions\n2. Call `mcp.setup_server()` after adding new endpoints to re-register all tools\n\nFor a working example, see [Reregister Tools Example](https://github.com/tadata-org/fastapi_mcp/blob/main/examples/05_reregister_tools_example.py).\n\n### Can I add custom tools other than FastAPI endpoints?\nCurrently, FastApiMCP only supports tools that are derived from FastAPI endpoints. If you need to add custom tools that don't correspond to API endpoints, you can:\n1. Create a FastAPI endpoint that wraps your custom functionality\n2. Contribute to the project by implementing custom tool support\n\nFollow the discussion in [issue #75](https://github.com/tadata-org/fastapi_mcp/issues/75) for updates on this feature request.\nIf you have specific use cases for custom tools, please share them in the issue to help guide the implementation.\n\n### How do I test my FastApiMCP server is working?\nTo verify your FastApiMCP server is working properly, you can use the MCP Inspector tool. Here's how:\n\n1. Start your FastAPI application\n2. Open a new terminal and run the MCP Inspector:\n   ```bash\n   npx @modelcontextprotocol/inspector\n   ```\n3. Connect to your MCP server by entering the mount path URL (default: `http://127.0.0.1:8000/mcp`)\n4. Navigate to the `Tools` section and click `List Tools` to see all available endpoints\n5. Test an endpoint by:\n   - Selecting a tool from the list\n   - Filling in any required parameters\n   - Clicking `Run Tool` to execute\n6. Check your server logs for additional debugging information if needed\n\nThis will help confirm that your MCP server is properly configured and your endpoints are accessible.\n\n## Development\n\n### Can I contribute to the project?\nYes! Please read our [CONTRIBUTING.md](https://github.com/tadata-org/fastapi_mcp/blob/main/CONTRIBUTING.md) file for detailed guidelines on how to contribute to the project and where to start.\n\n## Support\n\n### Where can I get help?\n- Check the documentation\n- Open an issue on GitHub\n- Join our community chat [MCParty Slack community](https://join.slack.com/t/themcparty/shared_invite/zt-30yxr1zdi-2FG~XjBA0xIgYSYuKe7~Xg)\n"
  },
  {
    "path": "docs/getting-started/best-practices.mdx",
    "content": "---\ntitle: Best Practices\nicon: thumbs-up\n---\n\nThis guide outlines best practices for converting standard APIs into Model Context Protocol (MCP) tools for use with AI agents. Proper tool design helps ensure LLMs can understand and safely use your APIs.\nBy following these best practices, you can build safer, more intuitive MCP tools that enhance the capabilities of LLM agents.\n\n\n## Tool Selection\n\n- **Be selective:**  \n  Avoid exposing every endpoint as a tool. LLM clients perform better with a limited number of well-defined tools, and providers often impose tool limits.\n\n- **Prioritize safety:**  \n  Do **not** expose `PUT` or `DELETE` endpoints unless absolutely necessary. LLMs are non-deterministic and could unintentionally alter or damage systems or databases.\n\n- **Focus on data retrieval:**  \n  Prefer `GET` endpoints that return data safely and predictably.\n\n- **Emphasize meaningful workflows:**  \n  Expose endpoints that reflect clear, goal-oriented tasks. Tools with focused actions are easier for agents to understand and use effectively.\n\n## Tool Naming\n\n- **Use short, descriptive names:**  \n  Helps LLMs select and use the right tool. Know that some MCP clients restrict tool name length. \n\n- **Follow naming constraints:**\n  - Must start with a letter\n  - Can include only letters, numbers, and underscores\n  - Avoid hyphens (e.g., AWS Nova does **not** support them)\n  - Use either `camelCase` or `snake_case` consistently across all tools\n\n- **Ensure uniqueness:**  \n  Each tool name should be unique and clearly indicate its function.\n\n## Documentation\n\n- **Describe every tool meaningfully:**  \n  Provide a clear and concise summary of what each tool does.\n\n- **Include usage examples and parameter descriptions:**  \n  These help LLMs understand how to use the tool correctly.\n\n- **Standardize documentation across tools:**  \n  Keep formatting and structure consistent to maintain quality and readability.\n\n\n"
  },
  {
    "path": "docs/getting-started/installation.mdx",
    "content": "---\ntitle: Installation\nicon: arrow-down-to-line\n---\n\n## Install FastAPI-MCP\n\nWe recommend using [uv](https://docs.astral.sh/uv/), a fast Python package installer:\n\n```bash\nuv add fastapi-mcp\n```\n\nAlternatively, you can install with `pip` or `uv pip`:\n\n<CodeGroup>\n    ```bash uv\n    uv pip install fastapi-mcp\n    ```\n\n    ```bash pip\n    pip install fastapi-mcp\n    ```\n</CodeGroup>\n"
  },
  {
    "path": "docs/getting-started/quickstart.mdx",
    "content": "---\ntitle: Quickstart\nicon: rocket\n---\n\nThis guide will help you quickly run your first MCP server using FastAPI-MCP.\n\nIf you haven't already installed FastAPI-MCP, follow the [installation instructions](/getting-started/installation).\n\n## Creating a basic MCP server\n\nTo create a basic MCP server, import or create a FastAPI app, wrap it with the `FastApiMCP` class and mount the MCP to your existing application:\n\n```python {2, 8, 11}\nfrom fastapi import FastAPI\nfrom fastapi_mcp import FastApiMCP\n\n# Create (or import) a FastAPI app\napp = FastAPI()\n\n# Create an MCP server based on this app\nmcp = FastApiMCP(app)\n\n# Mount the MCP server directly to your app\nmcp.mount_http()\n```\n\nFor more usage examples, see [Examples](https://github.com/tadata-org/fastapi_mcp/tree/main/examples) section in the project.\n\n## Running the server\n\nBy running your FastAPI, your MCP will run at `https://app.base.url/mcp`.\n\nFor example, by using uvicorn, add to your code:\n```python {9-11}\nfrom fastapi import FastAPI\nfrom fastapi_mcp import FastApiMCP\n\napp = FastAPI()\n\nmcp = FastApiMCP(app)\nmcp.mount_http()\n\nif __name__ == \"__main__\":\n    import uvicorn\n    uvicorn.run(app, host=\"0.0.0.0\", port=8000)\n```\nand run the server using `python fastapi_mcp_server.py`, which will serve you the MCP at `http://localhost:8000/mcp`.\n\n## Connecting a client to the MCP server\n\nOnce your FastAPI app with MCP integration is running, you would want to connect it to an MCP client.\n\n### Connecting to the MCP Server using SSE\n\nFor any MCP client supporting SSE, you will simply need to provide the MCP url.\n\nAll the most popular MCP clients (Claude Desktop, Cursor & Windsurf) use the following config format:\n\n```json\n{\n  \"mcpServers\": {\n    \"fastapi-mcp\": {\n      \"url\": \"http://localhost:8000/mcp\"\n    }\n  }\n}\n```\n\n### Connecting to the MCP Server using [mcp-remote](https://www.npmjs.com/package/mcp-remote)\n\nIf you want to support authentication, or your MCP client does not support SSE, we recommend using `mcp-remote` as a bridge.\n\n```json\n{\n  \"mcpServers\": {\n    \"fastapi-mcp\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"mcp-remote\",\n        \"http://localhost:8000/mcp\",\n        \"8080\"  // Optional port number. Necessary if you want your OAuth to work and you don't have dynamic client registration.\n      ]\n    }\n  }\n}\n```\n\n\n\n"
  },
  {
    "path": "docs/getting-started/welcome.mdx",
    "content": "---\ntitle: \"Welcome to FastAPI-MCP!\"\nsidebarTitle: \"Welcome!\"\ndescription: Expose your FastAPI endpoints as Model Context Protocol (MCP) tools, with Auth!\n\nicon: hand-wave\n---\n\nMCP (Model Context Protocol) is the emerging standard to define how AI agents communicate with applications. Using FastAPI-MCP, creating a secured  MCP server to your application takes only 3 lines of code:\n\n```python {2, 6, 7}\nfrom fastapi import FastAPI\nfrom fastapi_mcp import FastApiMCP\n\napp = FastAPI()\n\nmcp = FastApiMCP(app)\nmcp.mount_http()\n```\nThat's it! Your auto-generated MCP server is now available at `https://app.base.url/mcp`\n\n## Features\n\n- [**Authentication**](/advanced/auth) built in, using your existing FastAPI dependencies!\n\n- **FastAPI-native:** Not just another OpenAPI -> MCP converter\n\n- **Zero configuration** required - just point it at your FastAPI app and it works\n\n- **Preserving schemas** of your request models and response models\n\n- **Preserve documentation** of all your endpoints, just as it is in Swagger\n\n- [**Flexible deployment**](/advanced/deploy) - Mount your MCP server to the same app, or deploy separately\n\n- [**ASGI interface**](/advanced/asgi) - Uses FastAPI's ASGI interface directly for efficient internal communication\n\n## Hosted Solution\n\nIf you prefer a managed hosted solution check out [tadata.com](https://tadata.com).\n"
  },
  {
    "path": "examples/01_basic_usage_example.py",
    "content": "from examples.shared.apps.items import app  # The FastAPI app\nfrom examples.shared.setup import setup_logging\n\nfrom fastapi_mcp import FastApiMCP\n\nsetup_logging()\n\n# Add MCP server to the FastAPI app\nmcp = FastApiMCP(app)\n\n# Mount the MCP server to the FastAPI app\nmcp.mount_http()\n\n\nif __name__ == \"__main__\":\n    import uvicorn\n\n    uvicorn.run(app, host=\"0.0.0.0\", port=8000)\n"
  },
  {
    "path": "examples/02_full_schema_description_example.py",
    "content": "\"\"\"\nThis example shows how to describe the full response schema instead of just a response example.\n\"\"\"\n\nfrom examples.shared.apps.items import app  # The FastAPI app\nfrom examples.shared.setup import setup_logging\n\nfrom fastapi_mcp import FastApiMCP\n\nsetup_logging()\n\n# Add MCP server to the FastAPI app\nmcp = FastApiMCP(\n    app,\n    name=\"Item API MCP\",\n    description=\"MCP server for the Item API\",\n    describe_full_response_schema=True,  # Describe the full response JSON-schema instead of just a response example\n    describe_all_responses=True,  # Describe all the possible responses instead of just the success (2XX) response\n)\n\nmcp.mount_http()\n\nif __name__ == \"__main__\":\n    import uvicorn\n\n    uvicorn.run(app, host=\"0.0.0.0\", port=8000)\n"
  },
  {
    "path": "examples/03_custom_exposed_endpoints_example.py",
    "content": "\"\"\"\nThis example shows how to customize exposing endpoints by filtering operation IDs and tags.\nNotes on filtering:\n- You cannot use both `include_operations` and `exclude_operations` at the same time\n- You cannot use both `include_tags` and `exclude_tags` at the same time\n- You can combine operation filtering with tag filtering (e.g., use `include_operations` with `include_tags`)\n- When combining filters, a greedy approach will be taken. Endpoints matching either criteria will be included\n\"\"\"\n\nfrom examples.shared.apps.items import app  # The FastAPI app\nfrom examples.shared.setup import setup_logging\n\nfrom fastapi_mcp import FastApiMCP\n\nsetup_logging()\n\n# Examples demonstrating how to filter MCP tools by operation IDs and tags\n\n# Filter by including specific operation IDs\ninclude_operations_mcp = FastApiMCP(\n    app,\n    name=\"Item API MCP - Included Operations\",\n    include_operations=[\"get_item\", \"list_items\"],\n)\n\n# Filter by excluding specific operation IDs\nexclude_operations_mcp = FastApiMCP(\n    app,\n    name=\"Item API MCP - Excluded Operations\",\n    exclude_operations=[\"create_item\", \"update_item\", \"delete_item\"],\n)\n\n# Filter by including specific tags\ninclude_tags_mcp = FastApiMCP(\n    app,\n    name=\"Item API MCP - Included Tags\",\n    include_tags=[\"items\"],\n)\n\n# Filter by excluding specific tags\nexclude_tags_mcp = FastApiMCP(\n    app,\n    name=\"Item API MCP - Excluded Tags\",\n    exclude_tags=[\"search\"],\n)\n\n# Combine operation IDs and tags (include mode)\ncombined_include_mcp = FastApiMCP(\n    app,\n    name=\"Item API MCP - Combined Include\",\n    include_operations=[\"delete_item\"],\n    include_tags=[\"search\"],\n)\n\n# Mount all MCP servers with different paths\ninclude_operations_mcp.mount_http(mount_path=\"/include-operations-mcp\")\nexclude_operations_mcp.mount_http(mount_path=\"/exclude-operations-mcp\")\ninclude_tags_mcp.mount_http(mount_path=\"/include-tags-mcp\")\nexclude_tags_mcp.mount_http(mount_path=\"/exclude-tags-mcp\")\ncombined_include_mcp.mount_http(mount_path=\"/combined-include-mcp\")\n\nif __name__ == \"__main__\":\n    import uvicorn\n\n    print(\"Server is running with multiple MCP endpoints:\")\n    print(\" - /include-operations-mcp: Only get_item and list_items operations\")\n    print(\" - /exclude-operations-mcp: All operations except create_item, update_item, and delete_item\")\n    print(\" - /include-tags-mcp: Only operations with the 'items' tag\")\n    print(\" - /exclude-tags-mcp: All operations except those with the 'search' tag\")\n    print(\" - /combined-include-mcp: Operations with 'search' tag or delete_item operation\")\n    uvicorn.run(app, host=\"0.0.0.0\", port=8000)\n"
  },
  {
    "path": "examples/04_separate_server_example.py",
    "content": "\"\"\"\nThis example shows how to run the MCP server and the FastAPI app separately.\nYou can create an MCP server from one FastAPI app, and mount it to a different app.\n\"\"\"\n\nfrom fastapi import FastAPI\n\nfrom examples.shared.apps.items import app\nfrom examples.shared.setup import setup_logging\n\nfrom fastapi_mcp import FastApiMCP\n\nsetup_logging()\n\nMCP_SERVER_HOST = \"localhost\"\nMCP_SERVER_PORT = 8000\nITEMS_API_HOST = \"localhost\"\nITEMS_API_PORT = 8001\n\n\n# Take the FastAPI app only as a source for MCP server generation\nmcp = FastApiMCP(app)\n\n# Mount the MCP server to a separate FastAPI app\nmcp_app = FastAPI()\nmcp.mount_http(mcp_app)\n\n# Run the MCP server separately from the original FastAPI app.\n# It still works 🚀\n# Your original API is **not exposed**, only via the MCP server.\nif __name__ == \"__main__\":\n    import uvicorn\n\n    uvicorn.run(mcp_app, host=\"0.0.0.0\", port=8000)\n"
  },
  {
    "path": "examples/05_reregister_tools_example.py",
    "content": "\"\"\"\nThis example shows how to re-register tools if you add endpoints after the MCP server was created.\n\"\"\"\n\nfrom examples.shared.apps.items import app  # The FastAPI app\nfrom examples.shared.setup import setup_logging\n\nfrom fastapi_mcp import FastApiMCP\n\nsetup_logging()\n\nmcp = FastApiMCP(app)  # Add MCP server to the FastAPI app\nmcp.mount_http()  # MCP server\n\n\n# This endpoint will not be registered as a tool, since it was added after the MCP instance was created\n@app.get(\"/new/endpoint/\", operation_id=\"new_endpoint\", response_model=dict[str, str])\nasync def new_endpoint():\n    return {\"message\": \"Hello, world!\"}\n\n\n# But if you re-run the setup, the new endpoints will now be exposed.\nmcp.setup_server()\n\n\nif __name__ == \"__main__\":\n    import uvicorn\n\n    uvicorn.run(app, host=\"0.0.0.0\", port=8000)\n"
  },
  {
    "path": "examples/06_custom_mcp_router_example.py",
    "content": "\"\"\"\nThis example shows how to mount the MCP server to a specific APIRouter, giving a custom mount path.\n\"\"\"\n\nfrom examples.shared.apps.items import app  # The FastAPI app\nfrom examples.shared.setup import setup_logging\n\nfrom fastapi import APIRouter\nfrom fastapi_mcp import FastApiMCP\n\nsetup_logging()\n\nother_router = APIRouter(prefix=\"/other/route\")\napp.include_router(other_router)\n\nmcp = FastApiMCP(app)\n\n# Mount the MCP server to a specific router.\n# It will now only be available at `/other/route/mcp`\nmcp.mount_http(other_router)\n\n\nif __name__ == \"__main__\":\n    import uvicorn\n\n    uvicorn.run(app, host=\"0.0.0.0\", port=8000)\n"
  },
  {
    "path": "examples/07_configure_http_timeout_example.py",
    "content": "\"\"\"\nThis example shows how to configure the HTTP client timeout for the MCP server.\nIn case you have API endpoints that take longer than 5 seconds to respond, you can increase the timeout.\n\"\"\"\n\nfrom examples.shared.apps.items import app  # The FastAPI app\nfrom examples.shared.setup import setup_logging\n\nimport httpx\n\nfrom fastapi_mcp import FastApiMCP\n\nsetup_logging()\n\n\nmcp = FastApiMCP(app, http_client=httpx.AsyncClient(timeout=20))\nmcp.mount_http()\n\n\nif __name__ == \"__main__\":\n    import uvicorn\n\n    uvicorn.run(app, host=\"0.0.0.0\", port=8000)\n"
  },
  {
    "path": "examples/08_auth_example_token_passthrough.py",
    "content": "\"\"\"\nThis example shows how to reject any request without a valid token passed in the Authorization header.\n\nIn order to configure the auth header, the config file for the MCP server should looks like this:\n```json\n{\n  \"mcpServers\": {\n    \"remote-example\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"mcp-remote\",\n        \"http://localhost:8000/mcp\",\n        \"--header\",\n        \"Authorization:${AUTH_HEADER}\"\n      ]\n    },\n    \"env\": {\n      \"AUTH_HEADER\": \"Bearer <your-token>\"\n    }\n  }\n}\n```\n\"\"\"\n\nfrom examples.shared.apps.items import app  # The FastAPI app\nfrom examples.shared.setup import setup_logging\n\nfrom fastapi import Depends\nfrom fastapi.security import HTTPBearer\n\nfrom fastapi_mcp import FastApiMCP, AuthConfig\n\nsetup_logging()\n\n# Scheme for the Authorization header\ntoken_auth_scheme = HTTPBearer()\n\n\n# Create a private endpoint\n@app.get(\"/private\")\nasync def private(token=Depends(token_auth_scheme)):\n    return token.credentials\n\n\n# Create the MCP server with the token auth scheme\nmcp = FastApiMCP(\n    app,\n    name=\"Protected MCP\",\n    auth_config=AuthConfig(\n        dependencies=[Depends(token_auth_scheme)],\n    ),\n)\n\n# Mount the MCP server\nmcp.mount_http()\n\n\nif __name__ == \"__main__\":\n    import uvicorn\n\n    uvicorn.run(app, host=\"0.0.0.0\", port=8000)\n"
  },
  {
    "path": "examples/09_auth_example_auth0.py",
    "content": "from fastapi import FastAPI, Depends, HTTPException, Request, status\nfrom pydantic_settings import BaseSettings\nfrom typing import Any\nimport logging\n\nfrom fastapi_mcp import FastApiMCP, AuthConfig\n\nfrom examples.shared.auth import fetch_jwks_public_key\nfrom examples.shared.setup import setup_logging\n\n\nsetup_logging()\nlogger = logging.getLogger(__name__)\n\n\nclass Settings(BaseSettings):\n    \"\"\"\n    For this to work, you need an .env file in the root of the project with the following variables:\n    AUTH0_DOMAIN=your-tenant.auth0.com\n    AUTH0_AUDIENCE=https://your-tenant.auth0.com/api/v2/\n    AUTH0_CLIENT_ID=your-client-id\n    AUTH0_CLIENT_SECRET=your-client-secret\n    \"\"\"\n\n    auth0_domain: str  # Auth0 domain, e.g. \"your-tenant.auth0.com\"\n    auth0_audience: str  # Audience, e.g. \"https://your-tenant.auth0.com/api/v2/\"\n    auth0_client_id: str\n    auth0_client_secret: str\n\n    @property\n    def auth0_jwks_url(self):\n        return f\"https://{self.auth0_domain}/.well-known/jwks.json\"\n\n    @property\n    def auth0_oauth_metadata_url(self):\n        return f\"https://{self.auth0_domain}/.well-known/openid-configuration\"\n\n    class Config:\n        env_file = \".env\"\n\n\nsettings = Settings()  # type: ignore\n\n\nasync def lifespan(app: FastAPI):\n    app.state.jwks_public_key = await fetch_jwks_public_key(settings.auth0_jwks_url)\n    logger.info(f\"Auth0 client ID in settings: {settings.auth0_client_id}\")\n    logger.info(f\"Auth0 domain in settings: {settings.auth0_domain}\")\n    logger.info(f\"Auth0 audience in settings: {settings.auth0_audience}\")\n    yield\n\n\nasync def verify_auth(request: Request) -> dict[str, Any]:\n    try:\n        import jwt\n\n        auth_header = request.headers.get(\"authorization\", \"\")\n        if not auth_header.startswith(\"Bearer \"):\n            raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=\"Invalid authorization header\")\n\n        token = auth_header.split(\" \")[1]\n\n        header = jwt.get_unverified_header(token)\n\n        # Check if this is a JWE token (encrypted token)\n        if header.get(\"alg\") == \"dir\" and header.get(\"enc\") == \"A256GCM\":\n            raise ValueError(\n                \"Token is encrypted, offline validation not possible. \"\n                \"This is usually due to not specifying the audience when requesting the token.\"\n            )\n\n        # Otherwise, it's a JWT, we can validate it offline\n        if header.get(\"alg\") in [\"RS256\", \"HS256\"]:\n            claims = jwt.decode(\n                token,\n                app.state.jwks_public_key,\n                algorithms=[\"RS256\", \"HS256\"],\n                audience=settings.auth0_audience,\n                issuer=f\"https://{settings.auth0_domain}/\",\n                options={\"verify_signature\": True},\n            )\n            return claims\n\n    except Exception as e:\n        logger.error(f\"Auth error: {str(e)}\")\n\n    raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=\"Unauthorized\")\n\n\nasync def get_current_user_id(claims: dict = Depends(verify_auth)) -> str:\n    user_id = claims.get(\"sub\")\n\n    if not user_id:\n        logger.error(\"No user ID found in token\")\n        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=\"Unauthorized\")\n\n    return user_id\n\n\napp = FastAPI(lifespan=lifespan)\n\n\n@app.get(\"/api/public\", operation_id=\"public\")\nasync def public():\n    return {\"message\": \"This is a public route\"}\n\n\n@app.get(\"/api/protected\", operation_id=\"protected\")\nasync def protected(user_id: str = Depends(get_current_user_id)):\n    return {\"message\": f\"Hello, {user_id}!\", \"user_id\": user_id}\n\n\n# Set up FastAPI-MCP with Auth0 auth\nmcp = FastApiMCP(\n    app,\n    name=\"MCP With Auth0\",\n    description=\"Example of FastAPI-MCP with Auth0 authentication\",\n    auth_config=AuthConfig(\n        issuer=f\"https://{settings.auth0_domain}/\",\n        authorize_url=f\"https://{settings.auth0_domain}/authorize\",\n        oauth_metadata_url=settings.auth0_oauth_metadata_url,\n        audience=settings.auth0_audience,\n        client_id=settings.auth0_client_id,\n        client_secret=settings.auth0_client_secret,\n        dependencies=[Depends(verify_auth)],\n        setup_proxies=True,\n    ),\n)\n\n# Mount the MCP server\nmcp.mount_http()\n\n\nif __name__ == \"__main__\":\n    import uvicorn\n\n    uvicorn.run(app, host=\"0.0.0.0\", port=8000)\n"
  },
  {
    "path": "examples/README.md",
    "content": "# FastAPI-MCP Examples\n\nThe following examples demonstrate various features and usage patterns of FastAPI-MCP:\n\n1. [Basic Usage Example](01_basic_usage_example.py) - Basic FastAPI-MCP integration\n2. [Full Schema Description](02_full_schema_description_example.py) - Customizing schema descriptions\n3. [Custom Exposed Endpoints](03_custom_exposed_endpoints_example.py) - Controlling which endpoints are exposed\n4. [Separate Server](04_separate_server_example.py) - Deploying MCP server separately\n5. [Reregister Tools](05_reregister_tools_example.py) - Adding endpoints after MCP server creation\n6. [Custom MCP Router](06_custom_mcp_router_example.py) - Advanced routing configuration\n7. [Configure HTTP Timeout](07_configure_http_timeout_example.py) - Customizing timeout settings\n"
  },
  {
    "path": "examples/__init__.py",
    "content": ""
  },
  {
    "path": "examples/shared/__init__.py",
    "content": ""
  },
  {
    "path": "examples/shared/apps/__init__.py",
    "content": ""
  },
  {
    "path": "examples/shared/apps/items.py",
    "content": "\"\"\"\nSimple example of using FastAPI-MCP to add an MCP server to a FastAPI app.\n\"\"\"\n\nfrom fastapi import FastAPI, HTTPException, Query\nfrom pydantic import BaseModel\nfrom typing import List, Optional\n\n\napp = FastAPI()\n\n\nclass Item(BaseModel):\n    id: int\n    name: str\n    description: Optional[str] = None\n    price: float\n    tags: List[str] = []\n\n\nitems_db: dict[int, Item] = {}\n\n\n@app.get(\"/items/\", response_model=List[Item], tags=[\"items\"], operation_id=\"list_items\")\nasync def list_items(skip: int = 0, limit: int = 10):\n    \"\"\"\n    List all items in the database.\n\n    Returns a list of items, with pagination support.\n    \"\"\"\n    return list(items_db.values())[skip : skip + limit]\n\n\n@app.get(\"/items/{item_id}\", response_model=Item, tags=[\"items\"], operation_id=\"get_item\")\nasync def read_item(item_id: int):\n    \"\"\"\n    Get a specific item by its ID.\n\n    Raises a 404 error if the item does not exist.\n    \"\"\"\n    if item_id not in items_db:\n        raise HTTPException(status_code=404, detail=\"Item not found\")\n    return items_db[item_id]\n\n\n@app.post(\"/items/\", response_model=Item, tags=[\"items\"], operation_id=\"create_item\")\nasync def create_item(item: Item):\n    \"\"\"\n    Create a new item in the database.\n\n    Returns the created item with its assigned ID.\n    \"\"\"\n    items_db[item.id] = item\n    return item\n\n\n@app.put(\"/items/{item_id}\", response_model=Item, tags=[\"items\"], operation_id=\"update_item\")\nasync def update_item(item_id: int, item: Item):\n    \"\"\"\n    Update an existing item.\n\n    Raises a 404 error if the item does not exist.\n    \"\"\"\n    if item_id not in items_db:\n        raise HTTPException(status_code=404, detail=\"Item not found\")\n\n    item.id = item_id\n    items_db[item_id] = item\n    return item\n\n\n@app.delete(\"/items/{item_id}\", tags=[\"items\"], operation_id=\"delete_item\")\nasync def delete_item(item_id: int):\n    \"\"\"\n    Delete an item from the database.\n\n    Raises a 404 error if the item does not exist.\n    \"\"\"\n    if item_id not in items_db:\n        raise HTTPException(status_code=404, detail=\"Item not found\")\n\n    del items_db[item_id]\n    return {\"message\": \"Item deleted successfully\"}\n\n\n@app.get(\"/items/search/\", response_model=List[Item], tags=[\"search\"], operation_id=\"search_items\")\nasync def search_items(\n    q: Optional[str] = Query(None, description=\"Search query string\"),\n    min_price: Optional[float] = Query(None, description=\"Minimum price\"),\n    max_price: Optional[float] = Query(None, description=\"Maximum price\"),\n    tags: List[str] = Query([], description=\"Filter by tags\"),\n):\n    \"\"\"\n    Search for items with various filters.\n\n    Returns a list of items that match the search criteria.\n    \"\"\"\n    results = list(items_db.values())\n\n    if q:\n        q = q.lower()\n        results = [\n            item for item in results if q in item.name.lower() or (item.description and q in item.description.lower())\n        ]\n\n    if min_price is not None:\n        results = [item for item in results if item.price >= min_price]\n    if max_price is not None:\n        results = [item for item in results if item.price <= max_price]\n\n    if tags:\n        results = [item for item in results if all(tag in item.tags for tag in tags)]\n\n    return results\n\n\nsample_items = [\n    Item(id=1, name=\"Hammer\", description=\"A tool for hammering nails\", price=9.99, tags=[\"tool\", \"hardware\"]),\n    Item(id=2, name=\"Screwdriver\", description=\"A tool for driving screws\", price=7.99, tags=[\"tool\", \"hardware\"]),\n    Item(id=3, name=\"Wrench\", description=\"A tool for tightening bolts\", price=12.99, tags=[\"tool\", \"hardware\"]),\n    Item(id=4, name=\"Saw\", description=\"A tool for cutting wood\", price=19.99, tags=[\"tool\", \"hardware\", \"cutting\"]),\n    Item(id=5, name=\"Drill\", description=\"A tool for drilling holes\", price=49.99, tags=[\"tool\", \"hardware\", \"power\"]),\n]\nfor item in sample_items:\n    items_db[item.id] = item\n"
  },
  {
    "path": "examples/shared/auth.py",
    "content": "from jwt.algorithms import RSAAlgorithm\nfrom cryptography.hazmat.primitives import serialization\nfrom cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey\n\nimport logging\nimport httpx\n\nfrom examples.shared.setup import setup_logging\n\nsetup_logging()\n\nlogger = logging.getLogger(__name__)\n\n\nasync def fetch_jwks_public_key(url: str) -> str:\n    \"\"\"\n    Fetch JWKS from a given URL and extract the primary public key in PEM format.\n\n    Args:\n        url: The JWKS URL to fetch from\n\n    Returns:\n        PEM-formatted public key as a string\n    \"\"\"\n    logger.info(f\"Fetching JWKS from: {url}\")\n    async with httpx.AsyncClient() as client:\n        response = await client.get(url)\n        response.raise_for_status()\n        jwks_data = response.json()\n\n        if not jwks_data or \"keys\" not in jwks_data or not jwks_data[\"keys\"]:\n            logger.error(\"Invalid JWKS data format: missing or empty 'keys' array\")\n            raise ValueError(\"Invalid JWKS data format: missing or empty 'keys' array\")\n\n        # Just use the first key in the set\n        jwk = jwks_data[\"keys\"][0]\n\n        # Convert JWK to PEM format\n        public_key = RSAAlgorithm.from_jwk(jwk)\n        if isinstance(public_key, RSAPublicKey):\n            pem = public_key.public_bytes(\n                encoding=serialization.Encoding.PEM,\n                format=serialization.PublicFormat.SubjectPublicKeyInfo,\n            )\n            pem_str = pem.decode(\"utf-8\")\n            logger.info(\"Successfully extracted public key from JWKS\")\n            return pem_str\n        else:\n            logger.error(\"Invalid JWKS data format: expected RSA public key\")\n            raise ValueError(\"Invalid JWKS data format: expected RSA public key\")\n"
  },
  {
    "path": "examples/shared/setup.py",
    "content": "import logging\n\nfrom pydantic import BaseModel\n\n\nclass LoggingConfig(BaseModel):\n    LOGGER_NAME: str = \"fastapi_mcp\"\n    LOG_FORMAT: str = \"%(levelprefix)s %(asctime)s\\t[%(name)s] %(message)s\"\n    LOG_LEVEL: str = logging.getLevelName(logging.DEBUG)\n\n    version: int = 1\n    disable_existing_loggers: bool = False\n    formatters: dict = {\n        \"default\": {\n            \"()\": \"uvicorn.logging.DefaultFormatter\",\n            \"fmt\": LOG_FORMAT,\n            \"datefmt\": \"%Y-%m-%d %H:%M:%S\",\n        },\n    }\n    handlers: dict = {\n        \"default\": {\n            \"formatter\": \"default\",\n            \"class\": \"logging.StreamHandler\",\n            \"stream\": \"ext://sys.stdout\",\n        },\n    }\n    loggers: dict = {\n        \"\": {\"handlers\": [\"default\"], \"level\": LOG_LEVEL},  # Root logger\n        \"uvicorn\": {\"handlers\": [\"default\"], \"level\": LOG_LEVEL},\n        LOGGER_NAME: {\"handlers\": [\"default\"], \"level\": LOG_LEVEL},\n    }\n\n\ndef setup_logging():\n    from logging.config import dictConfig\n\n    logging_config = LoggingConfig()\n    dictConfig(logging_config.model_dump())\n"
  },
  {
    "path": "fastapi_mcp/__init__.py",
    "content": "\"\"\"\nFastAPI-MCP: Automatic MCP server generator for FastAPI applications.\n\nCreated by Tadata Inc. (https://github.com/tadata-org)\n\"\"\"\n\ntry:\n    from importlib.metadata import version\n\n    __version__ = version(\"fastapi-mcp\")\nexcept Exception:  # pragma: no cover\n    # Fallback for local development\n    __version__ = \"0.0.0.dev0\"  # pragma: no cover\n\nfrom .server import FastApiMCP\nfrom .types import AuthConfig, OAuthMetadata\n\n\n__all__ = [\n    \"FastApiMCP\",\n    \"AuthConfig\",\n    \"OAuthMetadata\",\n]\n"
  },
  {
    "path": "fastapi_mcp/auth/__init__.py",
    "content": ""
  },
  {
    "path": "fastapi_mcp/auth/proxy.py",
    "content": "from typing_extensions import Annotated, Doc\nfrom fastapi import FastAPI, HTTPException, Request, status\nfrom fastapi.responses import RedirectResponse\nimport httpx\nfrom typing import Optional\nimport logging\nfrom urllib.parse import urlencode\n\nfrom fastapi_mcp.types import (\n    ClientRegistrationRequest,\n    ClientRegistrationResponse,\n    AuthConfig,\n    OAuthMetadata,\n    OAuthMetadataDict,\n    StrHttpUrl,\n)\n\n\nlogger = logging.getLogger(__name__)\n\n\ndef setup_oauth_custom_metadata(\n    app: Annotated[FastAPI, Doc(\"The FastAPI app instance\")],\n    auth_config: Annotated[AuthConfig, Doc(\"The AuthConfig used\")],\n    metadata: Annotated[OAuthMetadataDict, Doc(\"The custom metadata specified in AuthConfig\")],\n    include_in_schema: Annotated[bool, Doc(\"Whether to include the metadata endpoint in your OpenAPI docs\")] = False,\n):\n    \"\"\"\n    Just serve the custom metadata provided to AuthConfig under the path specified in `metadata_path`.\n    \"\"\"\n\n    auth_config = AuthConfig.model_validate(auth_config)\n    metadata = OAuthMetadata.model_validate(metadata)\n\n    @app.get(\n        auth_config.metadata_path,\n        response_model=OAuthMetadata,\n        response_model_exclude_unset=True,\n        response_model_exclude_none=True,\n        include_in_schema=include_in_schema,\n        operation_id=\"oauth_custom_metadata\",\n    )\n    async def oauth_metadata_proxy():\n        return metadata\n\n\ndef setup_oauth_metadata_proxy(\n    app: Annotated[FastAPI, Doc(\"The FastAPI app instance\")],\n    metadata_url: Annotated[\n        str,\n        Doc(\n            \"\"\"\n            The URL of the OAuth provider's metadata endpoint that you want to proxy.\n            \"\"\"\n        ),\n    ],\n    path: Annotated[\n        str,\n        Doc(\n            \"\"\"\n            The path to mount the OAuth metadata endpoint at.\n\n            Clients will usually expect this to be /.well-known/oauth-authorization-server\n            \"\"\"\n        ),\n    ] = \"/.well-known/oauth-authorization-server\",\n    authorize_path: Annotated[\n        str,\n        Doc(\n            \"\"\"\n            The path to mount the authorize endpoint at.\n\n            Clients will usually expect this to be /oauth/authorize\n            \"\"\"\n        ),\n    ] = \"/oauth/authorize\",\n    register_path: Annotated[\n        Optional[str],\n        Doc(\n            \"\"\"\n            The path to mount the register endpoint at.\n\n            Clients will usually expect this to be /oauth/register\n            \"\"\"\n        ),\n    ] = None,\n    include_in_schema: Annotated[bool, Doc(\"Whether to include the metadata endpoint in your OpenAPI docs\")] = False,\n):\n    \"\"\"\n    Proxy for your OAuth provider's Metadata endpoint, just adding our (fake) registration endpoint.\n    \"\"\"\n\n    @app.get(\n        path,\n        response_model=OAuthMetadata,\n        response_model_exclude_unset=True,\n        response_model_exclude_none=True,\n        include_in_schema=include_in_schema,\n        operation_id=\"oauth_metadata_proxy\",\n    )\n    async def oauth_metadata_proxy(request: Request):\n        base_url = str(request.base_url).rstrip(\"/\")\n\n        # Fetch your OAuth provider's OpenID Connect metadata\n        async with httpx.AsyncClient() as client:\n            response = await client.get(metadata_url)\n            if response.status_code != 200:\n                logger.error(\n                    f\"Failed to fetch OAuth metadata from {metadata_url}: {response.status_code}. Response: {response.text}\"\n                )\n                raise HTTPException(\n                    status_code=status.HTTP_502_BAD_GATEWAY,\n                    detail=\"Failed to fetch OAuth metadata\",\n                )\n\n            oauth_metadata = response.json()\n\n        # Override the registration endpoint if provided\n        if register_path:\n            oauth_metadata[\"registration_endpoint\"] = f\"{base_url}{register_path}\"\n\n        # Replace your OAuth provider's authorize endpoint with our proxy\n        oauth_metadata[\"authorization_endpoint\"] = f\"{base_url}{authorize_path}\"\n\n        return OAuthMetadata.model_validate(oauth_metadata)\n\n\ndef setup_oauth_authorize_proxy(\n    app: Annotated[FastAPI, Doc(\"The FastAPI app instance\")],\n    client_id: Annotated[\n        str,\n        Doc(\n            \"\"\"\n            In case the client doesn't specify a client ID, this will be used as the default client ID on the\n            request to your OAuth provider.\n            \"\"\"\n        ),\n    ],\n    authorize_url: Annotated[\n        Optional[StrHttpUrl],\n        Doc(\n            \"\"\"\n            The URL of your OAuth provider's authorization endpoint.\n\n            Usually this is something like `https://app.example.com/oauth/authorize`.\n            \"\"\"\n        ),\n    ],\n    audience: Annotated[\n        Optional[str],\n        Doc(\n            \"\"\"\n            Currently (2025-04-21), some Auth-supporting MCP clients (like `npx mcp-remote`) might not specify the\n            audience when sending a request to your server.\n\n            This may cause unexpected behavior from your OAuth provider, so this is a workaround.\n\n            In case the client doesn't specify an audience, this will be used as the default audience on the\n            request to your OAuth provider.\n            \"\"\"\n        ),\n    ] = None,\n    default_scope: Annotated[\n        str,\n        Doc(\n            \"\"\"\n            Currently (2025-04-21), some Auth-supporting MCP clients (like `npx mcp-remote`) might not specify the\n            scope when sending a request to your server.\n\n            This may cause unexpected behavior from your OAuth provider, so this is a workaround.\n\n            Here is where you can optionally specify a default scope that will be sent to your OAuth provider in case\n            the client doesn't specify it.\n            \"\"\"\n        ),\n    ] = \"openid profile email\",\n    path: Annotated[str, Doc(\"The path to mount the authorize endpoint at\")] = \"/oauth/authorize\",\n    include_in_schema: Annotated[bool, Doc(\"Whether to include the authorize endpoint in your OpenAPI docs\")] = False,\n):\n    \"\"\"\n    Proxy for your OAuth provider's authorize endpoint that logs the requested scopes and adds\n    default scopes and the audience parameter if not provided.\n    \"\"\"\n\n    @app.get(\n        path,\n        include_in_schema=include_in_schema,\n    )\n    async def oauth_authorize_proxy(\n        response_type: str = \"code\",\n        client_id: Optional[str] = client_id,\n        redirect_uri: Optional[str] = None,\n        scope: str = \"\",\n        state: Optional[str] = None,\n        code_challenge: Optional[str] = None,\n        code_challenge_method: Optional[str] = None,\n        audience: Optional[str] = audience,\n    ):\n        if not scope:\n            logger.warning(\"Client didn't provide any scopes! Using default scopes.\")\n            scope = default_scope\n            logger.debug(f\"Default scope: {scope}\")\n\n        scopes = scope.split()\n        logger.debug(f\"Scopes passed: {scopes}\")\n        for required_scope in default_scope.split():\n            if required_scope not in scopes:\n                scopes.append(required_scope)\n\n        params = {\n            \"response_type\": response_type,\n            \"client_id\": client_id,\n            \"redirect_uri\": redirect_uri,\n            \"scope\": \" \".join(scopes),\n            \"audience\": audience,\n        }\n\n        if state:\n            params[\"state\"] = state\n        if code_challenge:\n            params[\"code_challenge\"] = code_challenge\n        if code_challenge_method:\n            params[\"code_challenge_method\"] = code_challenge_method\n\n        auth_url = f\"{authorize_url}?{urlencode(params)}\"\n\n        return RedirectResponse(url=auth_url)\n\n\ndef setup_oauth_fake_dynamic_register_endpoint(\n    app: Annotated[FastAPI, Doc(\"The FastAPI app instance\")],\n    client_id: Annotated[str, Doc(\"The client ID of the pre-registered client\")],\n    client_secret: Annotated[str, Doc(\"The client secret of the pre-registered client\")],\n    path: Annotated[str, Doc(\"The path to mount the register endpoint at\")] = \"/oauth/register\",\n    include_in_schema: Annotated[bool, Doc(\"Whether to include the register endpoint in your OpenAPI docs\")] = False,\n):\n    \"\"\"\n    A proxy for dynamic client registration endpoint.\n\n    In MCP 2025-03-26 Spec, it is recommended to support OAuth Dynamic Client Registration (RFC 7591).\n    Furthermore, `npx mcp-remote` which is the current de-facto client that supports MCP's up-to-date spec,\n    requires this endpoint to be present.\n\n    But, this is an overcomplication for most use cases.\n\n    So instead of actually implementing dynamic client registration, we just echo back the pre-registered\n    client ID and secret.\n\n    Use this if you don't need dynamic client registration, or if your OAuth provider doesn't support it.\n    \"\"\"\n\n    @app.post(\n        path,\n        response_model=ClientRegistrationResponse,\n        include_in_schema=include_in_schema,\n    )\n    async def oauth_register_proxy(request: ClientRegistrationRequest) -> ClientRegistrationResponse:\n        client_response = ClientRegistrationResponse(\n            client_name=request.client_name or \"MCP Server\",  # Name doesn't really affect functionality\n            client_id=client_id,\n            client_secret=client_secret,\n            redirect_uris=request.redirect_uris,  # Just echo back their requested URIs\n            grant_types=request.grant_types or [\"authorization_code\"],\n            token_endpoint_auth_method=request.token_endpoint_auth_method or \"none\",\n        )\n        return client_response\n"
  },
  {
    "path": "fastapi_mcp/openapi/__init__.py",
    "content": ""
  },
  {
    "path": "fastapi_mcp/openapi/convert.py",
    "content": "import json\nimport logging\nfrom typing import Any, Dict, List, Tuple\n\nimport mcp.types as types\n\nfrom .utils import (\n    clean_schema_for_display,\n    generate_example_from_schema,\n    resolve_schema_references,\n    get_single_param_type_from_schema,\n)\n\nlogger = logging.getLogger(__name__)\n\n\ndef convert_openapi_to_mcp_tools(\n    openapi_schema: Dict[str, Any],\n    describe_all_responses: bool = False,\n    describe_full_response_schema: bool = False,\n) -> Tuple[List[types.Tool], Dict[str, Dict[str, Any]]]:\n    \"\"\"\n    Convert OpenAPI operations to MCP tools.\n\n    Args:\n        openapi_schema: The OpenAPI schema\n        describe_all_responses: Whether to include all possible response schemas in tool descriptions\n        describe_full_response_schema: Whether to include full response schema in tool descriptions\n\n    Returns:\n        A tuple containing:\n        - A list of MCP tools\n        - A mapping of operation IDs to operation details for HTTP execution\n    \"\"\"\n    # Resolve all references in the schema at once\n    resolved_openapi_schema = resolve_schema_references(openapi_schema, openapi_schema)\n\n    tools = []\n    operation_map = {}\n\n    # Process each path in the OpenAPI schema\n    for path, path_item in resolved_openapi_schema.get(\"paths\", {}).items():\n        for method, operation in path_item.items():\n            # Skip non-HTTP methods\n            if method not in [\"get\", \"post\", \"put\", \"delete\", \"patch\"]:\n                logger.warning(f\"Skipping non-HTTP method: {method}\")\n                continue\n\n            # Get operation metadata\n            operation_id = operation.get(\"operationId\")\n            if not operation_id:\n                logger.warning(f\"Skipping operation with no operationId: {operation}\")\n                continue\n\n            # Save operation details for later HTTP calls\n            operation_map[operation_id] = {\n                \"path\": path,\n                \"method\": method,\n                \"parameters\": operation.get(\"parameters\", []),\n                \"request_body\": operation.get(\"requestBody\", {}),\n            }\n\n            summary = operation.get(\"summary\", \"\")\n            description = operation.get(\"description\", \"\")\n\n            # Build tool description\n            tool_description = f\"{summary}\" if summary else f\"{method.upper()} {path}\"\n            if description:\n                tool_description += f\"\\n\\n{description}\"\n\n            # Add response information to the description\n            responses = operation.get(\"responses\", {})\n            if responses:\n                response_info = \"\\n\\n### Responses:\\n\"\n\n                # Find the success response\n                success_codes = range(200, 300)\n                success_response = None\n                for status_code in success_codes:\n                    if str(status_code) in responses:\n                        success_response = responses[str(status_code)]\n                        break\n\n                # Get the list of responses to include\n                responses_to_include = responses\n                if not describe_all_responses and success_response:\n                    # If we're not describing all responses, only include the success response\n                    success_code = next((code for code in success_codes if str(code) in responses), None)\n                    if success_code:\n                        responses_to_include = {str(success_code): success_response}\n\n                # Process all selected responses\n                for status_code, response_data in responses_to_include.items():\n                    response_desc = response_data.get(\"description\", \"\")\n                    response_info += f\"\\n**{status_code}**: {response_desc}\"\n\n                    # Highlight if this is the main success response\n                    if response_data == success_response:\n                        response_info += \" (Success Response)\"\n\n                    # Add schema information if available\n                    if \"content\" in response_data:\n                        for content_type, content_data in response_data[\"content\"].items():\n                            if \"schema\" in content_data:\n                                schema = content_data[\"schema\"]\n                                response_info += f\"\\nContent-Type: {content_type}\"\n\n                                # Clean the schema for display\n                                display_schema = clean_schema_for_display(schema)\n\n                                # Try to get example response\n                                example_response = None\n\n                                # Check if content has examples\n                                if \"examples\" in content_data:\n                                    for example_key, example_data in content_data[\"examples\"].items():\n                                        if \"value\" in example_data:\n                                            example_response = example_data[\"value\"]\n                                            break\n                                # If content has example\n                                elif \"example\" in content_data:\n                                    example_response = content_data[\"example\"]\n\n                                # If we have an example response, add it to the docs\n                                if example_response:\n                                    response_info += \"\\n\\n**Example Response:**\\n```json\\n\"\n                                    response_info += json.dumps(example_response, indent=2)\n                                    response_info += \"\\n```\"\n                                # Otherwise generate an example from the schema\n                                else:\n                                    generated_example = generate_example_from_schema(display_schema)\n                                    if generated_example:\n                                        response_info += \"\\n\\n**Example Response:**\\n```json\\n\"\n                                        response_info += json.dumps(generated_example, indent=2)\n                                        response_info += \"\\n```\"\n\n                                # Only include full schema information if requested\n                                if describe_full_response_schema:\n                                    # Format schema information based on its type\n                                    if display_schema.get(\"type\") == \"array\" and \"items\" in display_schema:\n                                        items_schema = display_schema[\"items\"]\n\n                                        response_info += \"\\n\\n**Output Schema:** Array of items with the following structure:\\n```json\\n\"\n                                        response_info += json.dumps(items_schema, indent=2)\n                                        response_info += \"\\n```\"\n                                    elif \"properties\" in display_schema:\n                                        response_info += \"\\n\\n**Output Schema:**\\n```json\\n\"\n                                        response_info += json.dumps(display_schema, indent=2)\n                                        response_info += \"\\n```\"\n                                    else:\n                                        response_info += \"\\n\\n**Output Schema:**\\n```json\\n\"\n                                        response_info += json.dumps(display_schema, indent=2)\n                                        response_info += \"\\n```\"\n\n                tool_description += response_info\n\n            # Organize parameters by type\n            path_params = []\n            query_params = []\n            header_params = []\n            body_params = []\n\n            for param in operation.get(\"parameters\", []):\n                param_name = param.get(\"name\")\n                param_in = param.get(\"in\")\n                required = param.get(\"required\", False)\n\n                if param_in == \"path\":\n                    path_params.append((param_name, param))\n                elif param_in == \"query\":\n                    query_params.append((param_name, param))\n                elif param_in == \"header\":\n                    header_params.append((param_name, param))\n\n            # Process request body if present\n            request_body = operation.get(\"requestBody\", {})\n            if request_body and \"content\" in request_body:\n                content_type = next(iter(request_body[\"content\"]), None)\n                if content_type and \"schema\" in request_body[\"content\"][content_type]:\n                    schema = request_body[\"content\"][content_type][\"schema\"]\n                    if \"properties\" in schema:\n                        for prop_name, prop_schema in schema[\"properties\"].items():\n                            required = prop_name in schema.get(\"required\", [])\n                            body_params.append(\n                                (\n                                    prop_name,\n                                    {\n                                        \"name\": prop_name,\n                                        \"schema\": prop_schema,\n                                        \"required\": required,\n                                    },\n                                )\n                            )\n\n            # Create input schema properties for all parameters\n            properties = {}\n            required_props = []\n\n            # Add path parameters to properties\n            for param_name, param in path_params:\n                param_schema = param.get(\"schema\", {})\n                param_desc = param.get(\"description\", \"\")\n                param_required = param.get(\"required\", True)  # Path params are usually required\n\n                properties[param_name] = param_schema.copy()\n                properties[param_name][\"title\"] = param_name\n                if param_desc:\n                    properties[param_name][\"description\"] = param_desc\n\n                if \"type\" not in properties[param_name]:\n                    properties[param_name][\"type\"] = param_schema.get(\"type\", \"string\")\n\n                if param_required:\n                    required_props.append(param_name)\n\n            # Add query parameters to properties\n            for param_name, param in query_params:\n                param_schema = param.get(\"schema\", {})\n                param_desc = param.get(\"description\", \"\")\n                param_required = param.get(\"required\", False)\n\n                properties[param_name] = param_schema.copy()\n                properties[param_name][\"title\"] = param_name\n                if param_desc:\n                    properties[param_name][\"description\"] = param_desc\n\n                if \"type\" not in properties[param_name]:\n                    properties[param_name][\"type\"] = get_single_param_type_from_schema(param_schema)\n\n                if \"default\" in param_schema:\n                    properties[param_name][\"default\"] = param_schema[\"default\"]\n\n                if param_required:\n                    required_props.append(param_name)\n\n            # Add body parameters to properties\n            for param_name, param in body_params:\n                param_schema = param.get(\"schema\", {})\n                param_desc = param.get(\"description\", \"\")\n                param_required = param.get(\"required\", False)\n\n                properties[param_name] = param_schema.copy()\n                properties[param_name][\"title\"] = param_name\n                if param_desc:\n                    properties[param_name][\"description\"] = param_desc\n\n                if \"type\" not in properties[param_name]:\n                    properties[param_name][\"type\"] = get_single_param_type_from_schema(param_schema)\n\n                if \"default\" in param_schema:\n                    properties[param_name][\"default\"] = param_schema[\"default\"]\n\n                if param_required:\n                    required_props.append(param_name)\n\n            # Create a proper input schema for the tool\n            input_schema = {\"type\": \"object\", \"properties\": properties, \"title\": f\"{operation_id}Arguments\"}\n\n            if required_props:\n                input_schema[\"required\"] = required_props\n\n            # Create the MCP tool definition\n            tool = types.Tool(name=operation_id, description=tool_description, inputSchema=input_schema)\n\n            tools.append(tool)\n\n    return tools, operation_map\n"
  },
  {
    "path": "fastapi_mcp/openapi/utils.py",
    "content": "from typing import Any, Dict\n\n\ndef get_single_param_type_from_schema(param_schema: Dict[str, Any]) -> str:\n    \"\"\"\n    Get the type of a parameter from the schema.\n    If the schema is a union type, return the first type.\n    \"\"\"\n    if \"anyOf\" in param_schema:\n        types = {schema.get(\"type\") for schema in param_schema[\"anyOf\"] if schema.get(\"type\")}\n        if \"null\" in types:\n            types.remove(\"null\")\n        if types:\n            return next(iter(types))\n        return \"string\"\n    return param_schema.get(\"type\", \"string\")\n\n\ndef resolve_schema_references(schema_part: Dict[str, Any], reference_schema: Dict[str, Any]) -> Dict[str, Any]:\n    \"\"\"\n    Resolve schema references in OpenAPI schemas.\n\n    Args:\n        schema_part: The part of the schema being processed that may contain references\n        reference_schema: The complete schema used to resolve references from\n\n    Returns:\n        The schema with references resolved\n    \"\"\"\n    # Make a copy to avoid modifying the input schema\n    schema_part = schema_part.copy()\n\n    # Handle $ref directly in the schema\n    if \"$ref\" in schema_part:\n        ref_path = schema_part[\"$ref\"]\n        # Standard OpenAPI references are in the format \"#/components/schemas/ModelName\"\n        if ref_path.startswith(\"#/components/schemas/\"):\n            model_name = ref_path.split(\"/\")[-1]\n            if \"components\" in reference_schema and \"schemas\" in reference_schema[\"components\"]:\n                if model_name in reference_schema[\"components\"][\"schemas\"]:\n                    # Replace with the resolved schema\n                    ref_schema = reference_schema[\"components\"][\"schemas\"][model_name].copy()\n                    # Remove the $ref key and merge with the original schema\n                    schema_part.pop(\"$ref\")\n                    schema_part.update(ref_schema)\n\n    # Recursively resolve references in all dictionary values\n    for key, value in schema_part.items():\n        if isinstance(value, dict):\n            schema_part[key] = resolve_schema_references(value, reference_schema)\n        elif isinstance(value, list):\n            # Only process list items that are dictionaries since only they can contain refs\n            schema_part[key] = [\n                resolve_schema_references(item, reference_schema) if isinstance(item, dict) else item for item in value\n            ]\n\n    return schema_part\n\n\ndef clean_schema_for_display(schema: Dict[str, Any]) -> Dict[str, Any]:\n    \"\"\"\n    Clean up a schema for display by removing internal fields.\n\n    Args:\n        schema: The schema to clean\n\n    Returns:\n        The cleaned schema\n    \"\"\"\n    # Make a copy to avoid modifying the input schema\n    schema = schema.copy()\n\n    # Remove common internal fields that are not helpful for LLMs\n    fields_to_remove = [\n        \"allOf\",\n        \"anyOf\",\n        \"oneOf\",\n        \"nullable\",\n        \"discriminator\",\n        \"readOnly\",\n        \"writeOnly\",\n        \"xml\",\n        \"externalDocs\",\n    ]\n    for field in fields_to_remove:\n        if field in schema:\n            schema.pop(field)\n\n    # Process nested properties\n    if \"properties\" in schema:\n        for prop_name, prop_schema in schema[\"properties\"].items():\n            if isinstance(prop_schema, dict):\n                schema[\"properties\"][prop_name] = clean_schema_for_display(prop_schema)\n\n    # Process array items\n    if \"type\" in schema and schema[\"type\"] == \"array\" and \"items\" in schema:\n        if isinstance(schema[\"items\"], dict):\n            schema[\"items\"] = clean_schema_for_display(schema[\"items\"])\n\n    return schema\n\n\ndef generate_example_from_schema(schema: Dict[str, Any]) -> Any:\n    \"\"\"\n    Generate a simple example response from a JSON schema.\n\n    Args:\n        schema: The JSON schema to generate an example from\n\n    Returns:\n        An example object based on the schema\n    \"\"\"\n    if not schema or not isinstance(schema, dict):\n        return None\n\n    # Handle different types\n    schema_type = schema.get(\"type\")\n\n    if schema_type == \"object\":\n        result = {}\n        if \"properties\" in schema:\n            for prop_name, prop_schema in schema[\"properties\"].items():\n                # Generate an example for each property\n                prop_example = generate_example_from_schema(prop_schema)\n                if prop_example is not None:\n                    result[prop_name] = prop_example\n        return result\n\n    elif schema_type == \"array\":\n        if \"items\" in schema:\n            # Generate a single example item\n            item_example = generate_example_from_schema(schema[\"items\"])\n            if item_example is not None:\n                return [item_example]\n        return []\n\n    elif schema_type == \"string\":\n        # Check if there's a format\n        format_type = schema.get(\"format\")\n        if format_type == \"date-time\":\n            return \"2023-01-01T00:00:00Z\"\n        elif format_type == \"date\":\n            return \"2023-01-01\"\n        elif format_type == \"email\":\n            return \"user@example.com\"\n        elif format_type == \"uri\":\n            return \"https://example.com\"\n        # Use title or property name if available\n        return schema.get(\"title\", \"string\")\n\n    elif schema_type == \"integer\":\n        return 1\n\n    elif schema_type == \"number\":\n        return 1.0\n\n    elif schema_type == \"boolean\":\n        return True\n\n    elif schema_type == \"null\":\n        return None\n\n    # Default case\n    return None\n"
  },
  {
    "path": "fastapi_mcp/server.py",
    "content": "import json\nimport httpx\nfrom typing import Dict, Optional, Any, List, Union, Literal, Sequence\nfrom typing_extensions import Annotated, Doc\n\nfrom fastapi import FastAPI, Request, APIRouter, params\nfrom fastapi.openapi.utils import get_openapi\nfrom mcp.server.lowlevel.server import Server\nimport mcp.types as types\n\nfrom fastapi_mcp.openapi.convert import convert_openapi_to_mcp_tools\nfrom fastapi_mcp.transport.sse import FastApiSseTransport\nfrom fastapi_mcp.transport.http import FastApiHttpSessionManager\nfrom fastapi_mcp.types import HTTPRequestInfo, AuthConfig\n\nimport logging\n\n\nlogger = logging.getLogger(__name__)\n\n\nclass FastApiMCP:\n    \"\"\"\n    Create an MCP server from a FastAPI app.\n    \"\"\"\n\n    def __init__(\n        self,\n        fastapi: Annotated[\n            FastAPI,\n            Doc(\"The FastAPI application to create an MCP server from\"),\n        ],\n        name: Annotated[\n            Optional[str],\n            Doc(\"Name for the MCP server (defaults to app.title)\"),\n        ] = None,\n        description: Annotated[\n            Optional[str],\n            Doc(\"Description for the MCP server (defaults to app.description)\"),\n        ] = None,\n        describe_all_responses: Annotated[\n            bool,\n            Doc(\"Whether to include all possible response schemas in tool descriptions\"),\n        ] = False,\n        describe_full_response_schema: Annotated[\n            bool,\n            Doc(\"Whether to include full json schema for responses in tool descriptions\"),\n        ] = False,\n        http_client: Annotated[\n            Optional[httpx.AsyncClient],\n            Doc(\n                \"\"\"\n                Optional custom HTTP client to use for API calls to the FastAPI app.\n                Has to be an instance of `httpx.AsyncClient`.\n                \"\"\"\n            ),\n        ] = None,\n        include_operations: Annotated[\n            Optional[List[str]],\n            Doc(\"List of operation IDs to include as MCP tools. Cannot be used with exclude_operations.\"),\n        ] = None,\n        exclude_operations: Annotated[\n            Optional[List[str]],\n            Doc(\"List of operation IDs to exclude from MCP tools. Cannot be used with include_operations.\"),\n        ] = None,\n        include_tags: Annotated[\n            Optional[List[str]],\n            Doc(\"List of tags to include as MCP tools. Cannot be used with exclude_tags.\"),\n        ] = None,\n        exclude_tags: Annotated[\n            Optional[List[str]],\n            Doc(\"List of tags to exclude from MCP tools. Cannot be used with include_tags.\"),\n        ] = None,\n        auth_config: Annotated[\n            Optional[AuthConfig],\n            Doc(\"Configuration for MCP authentication\"),\n        ] = None,\n        headers: Annotated[\n            List[str],\n            Doc(\n                \"\"\"\n                List of HTTP header names to forward from the incoming MCP request into each tool invocation.\n                Only headers in this allowlist will be forwarded. Defaults to ['authorization'].\n                \"\"\"\n            ),\n        ] = [\"authorization\"],\n    ):\n        # Validate operation and tag filtering options\n        if include_operations is not None and exclude_operations is not None:\n            raise ValueError(\"Cannot specify both include_operations and exclude_operations\")\n\n        if include_tags is not None and exclude_tags is not None:\n            raise ValueError(\"Cannot specify both include_tags and exclude_tags\")\n\n        self.operation_map: Dict[str, Dict[str, Any]]\n        self.tools: List[types.Tool]\n        self.server: Server\n\n        self.fastapi = fastapi\n        self.name = name or self.fastapi.title or \"FastAPI MCP\"\n        self.description = description or self.fastapi.description\n\n        self._base_url = \"http://apiserver\"\n        self._describe_all_responses = describe_all_responses\n        self._describe_full_response_schema = describe_full_response_schema\n        self._include_operations = include_operations\n        self._exclude_operations = exclude_operations\n        self._include_tags = include_tags\n        self._exclude_tags = exclude_tags\n        self._auth_config = auth_config\n\n        if self._auth_config:\n            self._auth_config = self._auth_config.model_validate(self._auth_config)\n\n        self._http_client = http_client or httpx.AsyncClient(\n            transport=httpx.ASGITransport(app=self.fastapi, raise_app_exceptions=False),\n            base_url=self._base_url,\n            timeout=10.0,\n        )\n\n        self._forward_headers = {h.lower() for h in headers}\n        self._http_transport: FastApiHttpSessionManager | None = None  # Store reference to HTTP transport for cleanup\n\n        self.setup_server()\n\n    def setup_server(self) -> None:\n        openapi_schema = get_openapi(\n            title=self.fastapi.title,\n            version=self.fastapi.version,\n            openapi_version=self.fastapi.openapi_version,\n            description=self.fastapi.description,\n            routes=self.fastapi.routes,\n        )\n\n        all_tools, self.operation_map = convert_openapi_to_mcp_tools(\n            openapi_schema,\n            describe_all_responses=self._describe_all_responses,\n            describe_full_response_schema=self._describe_full_response_schema,\n        )\n\n        # Filter tools based on operation IDs and tags\n        self.tools = self._filter_tools(all_tools, openapi_schema)\n\n        mcp_server: Server = Server(self.name, self.description)\n\n        @mcp_server.list_tools()\n        async def handle_list_tools() -> List[types.Tool]:\n            return self.tools\n\n        @mcp_server.call_tool()\n        async def handle_call_tool(\n            name: str, arguments: Dict[str, Any]\n        ) -> List[Union[types.TextContent, types.ImageContent, types.EmbeddedResource]]:\n            # Extract HTTP request info from MCP context\n            http_request_info = None\n            try:\n                # Access the MCP server's request context to get the original HTTP Request\n                request_context = mcp_server.request_context\n\n                if request_context and hasattr(request_context, \"request\"):\n                    http_request = request_context.request\n\n                    if http_request and hasattr(http_request, \"method\"):\n                        http_request_info = HTTPRequestInfo(\n                            method=http_request.method,\n                            path=http_request.url.path,\n                            headers=dict(http_request.headers),\n                            cookies=http_request.cookies,\n                            query_params=dict(http_request.query_params),\n                            body=None,\n                        )\n                        logger.debug(\n                            f\"Extracted HTTP request info from context: {http_request_info.method} {http_request_info.path}\"\n                        )\n            except (LookupError, AttributeError) as e:\n                logger.error(f\"Could not extract HTTP request info from context: {e}\")\n\n            return await self._execute_api_tool(\n                client=self._http_client,\n                tool_name=name,\n                arguments=arguments,\n                operation_map=self.operation_map,\n                http_request_info=http_request_info,\n            )\n\n        self.server = mcp_server\n\n    def _register_mcp_connection_endpoint_sse(\n        self,\n        router: FastAPI | APIRouter,\n        transport: FastApiSseTransport,\n        mount_path: str,\n        dependencies: Optional[Sequence[params.Depends]],\n    ):\n        @router.get(mount_path, include_in_schema=False, operation_id=\"mcp_connection\", dependencies=dependencies)\n        async def handle_mcp_connection(request: Request):\n            async with transport.connect_sse(request.scope, request.receive, request._send) as (reader, writer):\n                await self.server.run(\n                    reader,\n                    writer,\n                    self.server.create_initialization_options(notification_options=None, experimental_capabilities={}),\n                    raise_exceptions=False,\n                )\n\n    def _register_mcp_messages_endpoint_sse(\n        self,\n        router: FastAPI | APIRouter,\n        transport: FastApiSseTransport,\n        mount_path: str,\n        dependencies: Optional[Sequence[params.Depends]],\n    ):\n        @router.post(\n            f\"{mount_path}/messages/\",\n            include_in_schema=False,\n            operation_id=\"mcp_messages\",\n            dependencies=dependencies,\n        )\n        async def handle_post_message(request: Request):\n            return await transport.handle_fastapi_post_message(request)\n\n    def _register_mcp_endpoints_sse(\n        self,\n        router: FastAPI | APIRouter,\n        transport: FastApiSseTransport,\n        mount_path: str,\n        dependencies: Optional[Sequence[params.Depends]],\n    ):\n        self._register_mcp_connection_endpoint_sse(router, transport, mount_path, dependencies)\n        self._register_mcp_messages_endpoint_sse(router, transport, mount_path, dependencies)\n\n    def _register_mcp_http_endpoint(\n        self,\n        router: FastAPI | APIRouter,\n        transport: FastApiHttpSessionManager,\n        mount_path: str,\n        dependencies: Optional[Sequence[params.Depends]],\n    ):\n        @router.api_route(\n            mount_path,\n            methods=[\"GET\", \"POST\", \"DELETE\"],\n            include_in_schema=False,\n            operation_id=\"mcp_http\",\n            dependencies=dependencies,\n        )\n        async def handle_mcp_streamable_http(request: Request):\n            return await transport.handle_fastapi_request(request)\n\n    def _register_mcp_endpoints_http(\n        self,\n        router: FastAPI | APIRouter,\n        transport: FastApiHttpSessionManager,\n        mount_path: str,\n        dependencies: Optional[Sequence[params.Depends]],\n    ):\n        self._register_mcp_http_endpoint(router, transport, mount_path, dependencies)\n\n    def _setup_auth_2025_03_26(self):\n        from fastapi_mcp.auth.proxy import (\n            setup_oauth_custom_metadata,\n            setup_oauth_metadata_proxy,\n            setup_oauth_authorize_proxy,\n            setup_oauth_fake_dynamic_register_endpoint,\n        )\n\n        if self._auth_config:\n            if self._auth_config.custom_oauth_metadata:\n                setup_oauth_custom_metadata(\n                    app=self.fastapi,\n                    auth_config=self._auth_config,\n                    metadata=self._auth_config.custom_oauth_metadata,\n                )\n\n            elif self._auth_config.setup_proxies:\n                assert self._auth_config.client_id is not None\n\n                metadata_url = self._auth_config.oauth_metadata_url\n                if not metadata_url:\n                    metadata_url = f\"{self._auth_config.issuer}{self._auth_config.metadata_path}\"\n\n                setup_oauth_metadata_proxy(\n                    app=self.fastapi,\n                    metadata_url=metadata_url,\n                    path=self._auth_config.metadata_path,\n                    register_path=\"/oauth/register\" if self._auth_config.setup_fake_dynamic_registration else None,\n                )\n                setup_oauth_authorize_proxy(\n                    app=self.fastapi,\n                    client_id=self._auth_config.client_id,\n                    authorize_url=self._auth_config.authorize_url,\n                    audience=self._auth_config.audience,\n                    default_scope=self._auth_config.default_scope,\n                )\n                if self._auth_config.setup_fake_dynamic_registration:\n                    assert self._auth_config.client_secret is not None\n                    setup_oauth_fake_dynamic_register_endpoint(\n                        app=self.fastapi,\n                        client_id=self._auth_config.client_id,\n                        client_secret=self._auth_config.client_secret,\n                    )\n\n    def _setup_auth(self):\n        if self._auth_config:\n            if self._auth_config.version == \"2025-03-26\":\n                self._setup_auth_2025_03_26()\n            else:\n                raise ValueError(\n                    f\"Unsupported MCP spec version: {self._auth_config.version}. Please check your AuthConfig.\"\n                )\n        else:\n            logger.info(\"No auth config provided, skipping auth setup\")\n\n    def mount_http(\n        self,\n        router: Annotated[\n            Optional[FastAPI | APIRouter],\n            Doc(\n                \"\"\"\n                The FastAPI app or APIRouter to mount the MCP server to. If not provided, the MCP\n                server will be mounted to the FastAPI app.\n                \"\"\"\n            ),\n        ] = None,\n        mount_path: Annotated[\n            str,\n            Doc(\n                \"\"\"\n                Path where the MCP server will be mounted.\n                Mount path is appended to the root path of FastAPI router, or to the prefix of APIRouter.\n                Defaults to '/mcp'.\n                \"\"\"\n            ),\n        ] = \"/mcp\",\n    ) -> None:\n        \"\"\"\n        Mount the MCP server with HTTP transport to **any** FastAPI app or APIRouter.\n\n        There is no requirement that the FastAPI app or APIRouter is the same as the one that the MCP\n        server was created from.\n        \"\"\"\n        # Normalize mount path\n        if not mount_path.startswith(\"/\"):\n            mount_path = f\"/{mount_path}\"\n        if mount_path.endswith(\"/\"):\n            mount_path = mount_path[:-1]\n\n        if not router:\n            router = self.fastapi\n\n        assert isinstance(router, (FastAPI, APIRouter)), f\"Invalid router type: {type(router)}\"\n\n        http_transport = FastApiHttpSessionManager(mcp_server=self.server)\n        dependencies = self._auth_config.dependencies if self._auth_config else None\n\n        self._register_mcp_endpoints_http(router, http_transport, mount_path, dependencies)\n        self._setup_auth()\n        self._http_transport = http_transport  # Store reference\n\n        # HACK: If we got a router and not a FastAPI instance, we need to re-include the router so that\n        # FastAPI will pick up the new routes we added. The problem with this approach is that we assume\n        # that the router is a sub-router of self.fastapi, which may not always be the case.\n        #\n        # TODO: Find a better way to do this.\n        if isinstance(router, APIRouter):\n            self.fastapi.include_router(router)\n\n        logger.info(f\"MCP HTTP server listening at {mount_path}\")\n\n    def mount_sse(\n        self,\n        router: Annotated[\n            Optional[FastAPI | APIRouter],\n            Doc(\n                \"\"\"\n                The FastAPI app or APIRouter to mount the MCP server to. If not provided, the MCP\n                server will be mounted to the FastAPI app.\n                \"\"\"\n            ),\n        ] = None,\n        mount_path: Annotated[\n            str,\n            Doc(\n                \"\"\"\n                Path where the MCP server will be mounted.\n                Mount path is appended to the root path of FastAPI router, or to the prefix of APIRouter.\n                Defaults to '/sse'.\n                \"\"\"\n            ),\n        ] = \"/sse\",\n    ) -> None:\n        \"\"\"\n        Mount the MCP server with SSE transport to **any** FastAPI app or APIRouter.\n\n        There is no requirement that the FastAPI app or APIRouter is the same as the one that the MCP\n        server was created from.\n        \"\"\"\n        # Normalize mount path\n        if not mount_path.startswith(\"/\"):\n            mount_path = f\"/{mount_path}\"\n        if mount_path.endswith(\"/\"):\n            mount_path = mount_path[:-1]\n\n        if not router:\n            router = self.fastapi\n\n        # Build the base path correctly for the SSE transport\n        assert isinstance(router, (FastAPI, APIRouter)), f\"Invalid router type: {type(router)}\"\n        base_path = mount_path if isinstance(router, FastAPI) else router.prefix + mount_path\n        messages_path = f\"{base_path}/messages/\"\n\n        sse_transport = FastApiSseTransport(messages_path)\n        dependencies = self._auth_config.dependencies if self._auth_config else None\n\n        self._register_mcp_endpoints_sse(router, sse_transport, mount_path, dependencies)\n        self._setup_auth()\n\n        # HACK: If we got a router and not a FastAPI instance, we need to re-include the router so that\n        # FastAPI will pick up the new routes we added. The problem with this approach is that we assume\n        # that the router is a sub-router of self.fastapi, which may not always be the case.\n        #\n        # TODO: Find a better way to do this.\n        if isinstance(router, APIRouter):\n            self.fastapi.include_router(router)\n\n        logger.info(f\"MCP SSE server listening at {mount_path}\")\n\n    def mount(\n        self,\n        router: Annotated[\n            Optional[FastAPI | APIRouter],\n            Doc(\n                \"\"\"\n                The FastAPI app or APIRouter to mount the MCP server to. If not provided, the MCP\n                server will be mounted to the FastAPI app.\n                \"\"\"\n            ),\n        ] = None,\n        mount_path: Annotated[\n            str,\n            Doc(\n                \"\"\"\n                Path where the MCP server will be mounted.\n                Mount path is appended to the root path of FastAPI router, or to the prefix of APIRouter.\n                Defaults to '/mcp'.\n                \"\"\"\n            ),\n        ] = \"/mcp\",\n        transport: Annotated[\n            Literal[\"sse\"],\n            Doc(\n                \"\"\"\n                The transport type for the MCP server. Currently only 'sse' is supported.\n                This parameter is deprecated.\n                \"\"\"\n            ),\n        ] = \"sse\",\n    ) -> None:\n        \"\"\"\n        [DEPRECATED] Mount the MCP server to **any** FastAPI app or APIRouter.\n\n        This method is deprecated and will be removed in a future version.\n        Use mount_http() for HTTP transport (recommended) or mount_sse() for SSE transport instead.\n\n        For backwards compatibility, this method defaults to SSE transport.\n\n        There is no requirement that the FastAPI app or APIRouter is the same as the one that the MCP\n        server was created from.\n        \"\"\"\n        import warnings\n\n        warnings.warn(\n            \"mount() is deprecated and will be removed in a future version. \"\n            \"Use mount_http() for HTTP transport (recommended) or mount_sse() for SSE transport instead.\",\n            DeprecationWarning,\n            stacklevel=2,\n        )\n\n        if transport == \"sse\":\n            self.mount_sse(router, mount_path)\n        else:  # pragma: no cover\n            raise ValueError(  # pragma: no cover\n                f\"Unsupported transport: {transport}. Use mount_sse() or mount_http() instead.\"\n            )\n\n    async def _execute_api_tool(\n        self,\n        client: Annotated[httpx.AsyncClient, Doc(\"httpx client to use in API calls\")],\n        tool_name: Annotated[str, Doc(\"The name of the tool to execute\")],\n        arguments: Annotated[Dict[str, Any], Doc(\"The arguments for the tool\")],\n        operation_map: Annotated[Dict[str, Dict[str, Any]], Doc(\"A mapping from tool names to operation details\")],\n        http_request_info: Annotated[\n            Optional[HTTPRequestInfo],\n            Doc(\"HTTP request info to forward to the actual API call\"),\n        ] = None,\n    ) -> List[Union[types.TextContent, types.ImageContent, types.EmbeddedResource]]:\n        \"\"\"\n        Execute an MCP tool by making an HTTP request to the corresponding API endpoint.\n\n        Returns:\n            The result as MCP content types\n        \"\"\"\n        if tool_name not in operation_map:\n            raise Exception(f\"Unknown tool: {tool_name}\")\n\n        operation = operation_map[tool_name]\n        path: str = operation[\"path\"]\n        method: str = operation[\"method\"]\n        parameters: List[Dict[str, Any]] = operation.get(\"parameters\", [])\n        arguments = arguments.copy() if arguments else {}  # Deep copy arguments to avoid mutating the original\n\n        for param in parameters:\n            if param.get(\"in\") == \"path\" and param.get(\"name\") in arguments:\n                param_name = param.get(\"name\", None)\n                if param_name is None:\n                    raise ValueError(f\"Parameter name is None for parameter: {param}\")\n                path = path.replace(f\"{{{param_name}}}\", str(arguments.pop(param_name)))\n\n        query = {}\n        for param in parameters:\n            if param.get(\"in\") == \"query\" and param.get(\"name\") in arguments:\n                param_name = param.get(\"name\", None)\n                if param_name is None:\n                    raise ValueError(f\"Parameter name is None for parameter: {param}\")\n                query[param_name] = arguments.pop(param_name)\n\n        headers = {}\n        for param in parameters:\n            if param.get(\"in\") == \"header\" and param.get(\"name\") in arguments:\n                param_name = param.get(\"name\", None)\n                if param_name is None:\n                    raise ValueError(f\"Parameter name is None for parameter: {param}\")\n                headers[param_name] = arguments.pop(param_name)\n\n        # Forward headers that are in the allowlist\n        if http_request_info and http_request_info.headers:\n            for name, value in http_request_info.headers.items():\n                # case-insensitive check for allowed headers\n                if name.lower() in self._forward_headers:\n                    headers[name] = value\n\n        body = arguments if arguments else None\n\n        try:\n            logger.debug(f\"Making {method.upper()} request to {path}\")\n            response = await self._request(client, method, path, query, headers, body)\n\n            # TODO: Better typing for the AsyncClientProtocol. It should return a ResponseProtocol that has a json() method that returns a dict/list/etc.\n            try:\n                result = response.json()\n                result_text = json.dumps(result, indent=2, ensure_ascii=False)\n            except json.JSONDecodeError:\n                if hasattr(response, \"text\"):\n                    result_text = response.text\n                else:\n                    result_text = response.content\n\n            # If not raising an exception, the MCP server will return the result as a regular text response, without marking it as an error.\n            # TODO: Use a raise_for_status() method on the response (it needs to also be implemented in the AsyncClientProtocol)\n            if 400 <= response.status_code < 600:\n                raise Exception(\n                    f\"Error calling {tool_name}. Status code: {response.status_code}. Response: {response.text}\"\n                )\n\n            try:\n                return [types.TextContent(type=\"text\", text=result_text)]\n            except ValueError:\n                return [types.TextContent(type=\"text\", text=result_text)]\n\n        except Exception as e:\n            logger.exception(f\"Error calling {tool_name}\")\n            raise e\n\n    async def _request(\n        self,\n        client: httpx.AsyncClient,\n        method: str,\n        path: str,\n        query: Dict[str, Any],\n        headers: Dict[str, str],\n        body: Optional[Any],\n    ) -> Any:\n        if method.lower() == \"get\":\n            return await client.get(path, params=query, headers=headers)\n        elif method.lower() == \"post\":\n            return await client.post(path, params=query, headers=headers, json=body)\n        elif method.lower() == \"put\":\n            return await client.put(path, params=query, headers=headers, json=body)\n        elif method.lower() == \"delete\":\n            return await client.delete(path, params=query, headers=headers)\n        elif method.lower() == \"patch\":\n            return await client.patch(path, params=query, headers=headers, json=body)\n        else:\n            raise ValueError(f\"Unsupported HTTP method: {method}\")\n\n    def _filter_tools(self, tools: List[types.Tool], openapi_schema: Dict[str, Any]) -> List[types.Tool]:\n        \"\"\"\n        Filter tools based on operation IDs and tags.\n\n        Args:\n            tools: List of tools to filter\n            openapi_schema: The OpenAPI schema\n\n        Returns:\n            Filtered list of tools\n        \"\"\"\n        if (\n            self._include_operations is None\n            and self._exclude_operations is None\n            and self._include_tags is None\n            and self._exclude_tags is None\n        ):\n            return tools\n\n        operations_by_tag: Dict[str, List[str]] = {}\n        for path, path_item in openapi_schema.get(\"paths\", {}).items():\n            for method, operation in path_item.items():\n                if method not in [\"get\", \"post\", \"put\", \"delete\", \"patch\"]:\n                    continue\n\n                operation_id = operation.get(\"operationId\")\n                if not operation_id:\n                    continue\n\n                tags = operation.get(\"tags\", [])\n                for tag in tags:\n                    if tag not in operations_by_tag:\n                        operations_by_tag[tag] = []\n                    operations_by_tag[tag].append(operation_id)\n\n        operations_to_include = set()\n\n        if self._include_operations is not None:\n            operations_to_include.update(self._include_operations)\n        elif self._exclude_operations is not None:\n            all_operations = {tool.name for tool in tools}\n            operations_to_include.update(all_operations - set(self._exclude_operations))\n\n        if self._include_tags is not None:\n            for tag in self._include_tags:\n                operations_to_include.update(operations_by_tag.get(tag, []))\n        elif self._exclude_tags is not None:\n            excluded_operations = set()\n            for tag in self._exclude_tags:\n                excluded_operations.update(operations_by_tag.get(tag, []))\n\n            all_operations = {tool.name for tool in tools}\n            operations_to_include.update(all_operations - excluded_operations)\n\n        filtered_tools = [tool for tool in tools if tool.name in operations_to_include]\n\n        if filtered_tools:\n            filtered_operation_ids = {tool.name for tool in filtered_tools}\n            self.operation_map = {\n                op_id: details for op_id, details in self.operation_map.items() if op_id in filtered_operation_ids\n            }\n\n        return filtered_tools\n"
  },
  {
    "path": "fastapi_mcp/transport/__init__.py",
    "content": ""
  },
  {
    "path": "fastapi_mcp/transport/http.py",
    "content": "import logging\nimport asyncio\n\nfrom fastapi import Request, Response, HTTPException\nfrom mcp.server.lowlevel.server import Server\nfrom mcp.server.streamable_http_manager import StreamableHTTPSessionManager, EventStore\nfrom mcp.server.transport_security import TransportSecuritySettings\n\nlogger = logging.getLogger(__name__)\n\n\nclass FastApiHttpSessionManager:\n    \"\"\"\n    FastAPI-native wrapper around StreamableHTTPSessionManager\n    \"\"\"\n\n    def __init__(\n        self,\n        mcp_server: Server,\n        event_store: EventStore | None = None,\n        json_response: bool = True,  # Default to JSON for HTTP transport\n        security_settings: TransportSecuritySettings | None = None,\n    ):\n        self.mcp_server = mcp_server\n        self.event_store = event_store\n        self.json_response = json_response\n        self.security_settings = security_settings\n        self._session_manager: StreamableHTTPSessionManager | None = None\n        self._manager_task: asyncio.Task | None = None\n        self._manager_started = False\n        self._startup_lock = asyncio.Lock()\n\n    async def _ensure_session_manager_started(self) -> None:\n        \"\"\"\n        Ensure the session manager is started.\n\n        This is called lazily on the first request to start the session manager\n        if it hasn't been started yet.\n        \"\"\"\n        if self._manager_started:\n            return\n\n        async with self._startup_lock:\n            if self._manager_started:\n                return\n\n            logger.debug(\"Starting StreamableHTTP session manager\")\n\n            # Create the session manager\n            # Note: We don't use stateless=True because we want to support sessions\n            # but sessions are optional as per the MCP spec\n            self._session_manager = StreamableHTTPSessionManager(\n                app=self.mcp_server,\n                event_store=self.event_store,\n                json_response=self.json_response,\n                stateless=False,  # Always support sessions, but they're optional\n                security_settings=self.security_settings,\n            )\n\n            # Start the session manager in a background task\n            async def run_session_manager():\n                try:\n                    async with self._session_manager.run():\n                        logger.info(\"StreamableHTTP session manager is running\")\n                        # Keep running until cancelled\n                        await asyncio.Event().wait()\n                except asyncio.CancelledError:\n                    logger.info(\"StreamableHTTP session manager is shutting down\")\n                    raise\n                except Exception:\n                    logger.exception(\"Error in StreamableHTTP session manager\")\n                    raise\n\n            self._manager_task = asyncio.create_task(run_session_manager())\n            self._manager_started = True\n\n            # Give the session manager a moment to initialize\n            await asyncio.sleep(0.1)\n\n    async def handle_fastapi_request(self, request: Request) -> Response:\n        \"\"\"\n        Handle a FastAPI request by delegating to the session manager.\n\n        This converts FastAPI's Request/Response to ASGI scope/receive/send\n        and then converts the result back to a FastAPI Response.\n        \"\"\"\n        # Ensure session manager is started\n        await self._ensure_session_manager_started()\n\n        if not self._session_manager:\n            raise HTTPException(status_code=500, detail=\"Session manager not initialized\")\n\n        logger.debug(f\"Handling FastAPI request: {request.method} {request.url.path}\")\n\n        # Capture the response from the session manager\n        response_started = False\n        response_status = 200\n        response_headers = []\n        response_body = b\"\"\n\n        async def send_callback(message):\n            nonlocal response_started, response_status, response_headers, response_body\n\n            if message[\"type\"] == \"http.response.start\":\n                response_started = True\n                response_status = message[\"status\"]\n                response_headers = message.get(\"headers\", [])\n            elif message[\"type\"] == \"http.response.body\":\n                response_body += message.get(\"body\", b\"\")\n\n        try:\n            # Delegate to the session manager's handle_request method\n            await self._session_manager.handle_request(request.scope, request.receive, send_callback)\n\n            # Convert the captured ASGI response to a FastAPI Response\n            headers_dict = {name.decode(): value.decode() for name, value in response_headers}\n\n            return Response(\n                content=response_body,\n                status_code=response_status,\n                headers=headers_dict,\n            )\n\n        except Exception:\n            logger.exception(\"Error in StreamableHTTPSessionManager\")\n            raise HTTPException(status_code=500, detail=\"Internal server error\")\n\n    async def shutdown(self) -> None:\n        \"\"\"Clean up the session manager and background task.\"\"\"\n        if self._manager_task and not self._manager_task.done():\n            self._manager_task.cancel()\n            try:\n                await self._manager_task\n            except asyncio.CancelledError:\n                pass\n        self._manager_started = False\n"
  },
  {
    "path": "fastapi_mcp/transport/sse.py",
    "content": "from uuid import UUID\nimport logging\nfrom typing import Union\n\nfrom anyio.streams.memory import MemoryObjectSendStream\nfrom fastapi import Request, Response, BackgroundTasks, HTTPException\nfrom fastapi.responses import JSONResponse\nfrom mcp.shared.message import SessionMessage, ServerMessageMetadata\nfrom pydantic import ValidationError\nfrom mcp.server.sse import SseServerTransport\nfrom mcp.types import JSONRPCMessage, JSONRPCError, ErrorData\n\n\nlogger = logging.getLogger(__name__)\n\n\nclass FastApiSseTransport(SseServerTransport):\n    async def handle_fastapi_post_message(self, request: Request) -> Response:\n        \"\"\"\n        A reimplementation of the handle_post_message method of SseServerTransport\n        that integrates better with FastAPI.\n\n        A few good reasons for doing this:\n        1. Avoid mounting a whole Starlette app and instead use a more FastAPI-native\n           approach. Mounting has some known issues and limitations.\n        2. Avoid re-constructing the scope, receive, and send from the request, as done\n           in the original implementation.\n        3. Use FastAPI's native response handling mechanisms and exception patterns to\n           avoid unexpected rabbit holes.\n\n        The combination of mounting a whole Starlette app and reconstructing the scope\n        and send from the request proved to be especially error-prone for us when using\n        tracing tools like Sentry, which had destructive effects on the request object\n        when using the original implementation.\n        \"\"\"\n\n        logger.debug(\"Handling POST message SSE\")\n\n        session_id_param = request.query_params.get(\"session_id\")\n        if session_id_param is None:\n            logger.warning(\"Received request without session_id\")\n            raise HTTPException(status_code=400, detail=\"session_id is required\")\n\n        try:\n            session_id = UUID(hex=session_id_param)\n            logger.debug(f\"Parsed session ID: {session_id}\")\n        except ValueError:\n            logger.warning(f\"Received invalid session ID: {session_id_param}\")\n            raise HTTPException(status_code=400, detail=\"Invalid session ID\")\n\n        writer = self._read_stream_writers.get(session_id)\n        if not writer:\n            logger.warning(f\"Could not find session for ID: {session_id}\")\n            raise HTTPException(status_code=404, detail=\"Could not find session\")\n\n        body = await request.body()\n        logger.debug(f\"Received JSON: {body.decode()}\")\n\n        try:\n            message = JSONRPCMessage.model_validate_json(body)\n\n            logger.debug(f\"Validated client message: {message}\")\n        except ValidationError as err:\n            logger.error(f\"Failed to parse message: {err}\")\n            # Create background task to send error\n            background_tasks = BackgroundTasks()\n            background_tasks.add_task(self._send_message_safely, writer, err)\n            response = JSONResponse(content={\"error\": \"Could not parse message\"}, status_code=400)\n            response.background = background_tasks\n            return response\n        except Exception as e:\n            logger.error(f\"Error processing request body: {e}\")\n            raise HTTPException(status_code=400, detail=\"Invalid request body\")\n\n        # Create background task to send message with proper request context metadata\n        background_tasks = BackgroundTasks()\n        metadata = ServerMessageMetadata(request_context=request)\n        session_message = SessionMessage(message, metadata=metadata)\n        background_tasks.add_task(self._send_message_safely, writer, session_message)\n        logger.debug(\"Accepting message, will send in background\")\n\n        # Return response with background task\n        response = JSONResponse(content={\"message\": \"Accepted\"}, status_code=202)\n        response.background = background_tasks\n        return response\n\n    async def _send_message_safely(\n        self, writer: MemoryObjectSendStream[SessionMessage], message: Union[SessionMessage, ValidationError]\n    ):\n        \"\"\"Send a message to the writer, avoiding ASGI race conditions\"\"\"\n\n        try:\n            logger.debug(f\"Sending message to writer from background task: {message}\")\n\n            if isinstance(message, ValidationError):\n                # Convert ValidationError to JSONRPCError\n                error_data = ErrorData(\n                    code=-32700,  # Parse error code in JSON-RPC\n                    message=\"Parse error\",\n                    data={\"validation_error\": str(message)},\n                )\n                json_rpc_error = JSONRPCError(\n                    jsonrpc=\"2.0\",\n                    id=\"unknown\",  # We don't know the ID from the invalid request\n                    error=error_data,\n                )\n                error_message = SessionMessage(JSONRPCMessage(root=json_rpc_error))\n                await writer.send(error_message)\n            else:\n                await writer.send(message)\n        except Exception as e:\n            logger.error(f\"Error sending message to writer: {e}\")\n"
  },
  {
    "path": "fastapi_mcp/types.py",
    "content": "import time\nfrom typing import Any, Dict, Annotated, Union, Optional, Sequence, Literal, List\nfrom typing_extensions import Doc\nfrom pydantic import (\n    BaseModel,\n    ConfigDict,\n    HttpUrl,\n    field_validator,\n    model_validator,\n)\nfrom pydantic.main import IncEx\nfrom fastapi import params\n\n\nStrHttpUrl = Annotated[Union[str, HttpUrl], HttpUrl]\n\n\nclass BaseType(BaseModel):\n    model_config = ConfigDict(extra=\"ignore\", arbitrary_types_allowed=True)\n\n\nclass HTTPRequestInfo(BaseType):\n    method: str\n    path: str\n    headers: Dict[str, str]\n    cookies: Dict[str, str]\n    query_params: Dict[str, str]\n    body: Any\n\n\nclass OAuthMetadata(BaseType):\n    \"\"\"OAuth 2.0 Server Metadata according to RFC 8414\"\"\"\n\n    issuer: Annotated[\n        StrHttpUrl,\n        Doc(\n            \"\"\"\n            The authorization server's issuer identifier, which is a URL that uses the https scheme.\n            \"\"\"\n        ),\n    ]\n\n    authorization_endpoint: Annotated[\n        Optional[StrHttpUrl],\n        Doc(\n            \"\"\"\n            URL of the authorization server's authorization endpoint.\n            \"\"\"\n        ),\n    ] = None\n\n    token_endpoint: Annotated[\n        StrHttpUrl,\n        Doc(\n            \"\"\"\n            URL of the authorization server's token endpoint.\n            \"\"\"\n        ),\n    ]\n\n    scopes_supported: Annotated[\n        List[str],\n        Doc(\n            \"\"\"\n            List of OAuth 2.0 scopes that the authorization server supports.\n            \"\"\"\n        ),\n    ] = [\"openid\", \"profile\", \"email\"]\n\n    response_types_supported: Annotated[\n        List[str],\n        Doc(\n            \"\"\"\n            List of the OAuth 2.0 response_type values that the authorization server supports.\n            \"\"\"\n        ),\n    ] = [\"code\"]\n\n    grant_types_supported: Annotated[\n        List[str],\n        Doc(\n            \"\"\"\n            List of the OAuth 2.0 grant type values that the authorization server supports.\n            \"\"\"\n        ),\n    ] = [\"authorization_code\", \"client_credentials\"]\n\n    token_endpoint_auth_methods_supported: Annotated[\n        List[str],\n        Doc(\n            \"\"\"\n            List of client authentication methods supported by the token endpoint.\n            \"\"\"\n        ),\n    ] = [\"none\"]\n\n    code_challenge_methods_supported: Annotated[\n        List[str],\n        Doc(\n            \"\"\"\n            List of PKCE code challenge methods supported by the authorization server.\n            \"\"\"\n        ),\n    ] = [\"S256\"]\n\n    registration_endpoint: Annotated[\n        Optional[StrHttpUrl],\n        Doc(\n            \"\"\"\n            URL of the authorization server's client registration endpoint.\n            \"\"\"\n        ),\n    ] = None\n\n    @field_validator(\n        \"scopes_supported\",\n        \"response_types_supported\",\n        \"grant_types_supported\",\n        \"token_endpoint_auth_methods_supported\",\n        \"code_challenge_methods_supported\",\n    )\n    @classmethod\n    def validate_non_empty_lists(cls, v, info):\n        if not v:\n            raise ValueError(f\"{info.field_name} cannot be empty\")\n\n        return v\n\n    @model_validator(mode=\"after\")\n    def validate_endpoints_for_grant_types(self):\n        if \"authorization_code\" in self.grant_types_supported and not self.authorization_endpoint:\n            raise ValueError(\"authorization_endpoint is required when authorization_code grant type is supported\")\n        return self\n\n    def model_dump(\n        self,\n        *,\n        mode: Literal[\"json\", \"python\"] | str = \"python\",\n        include: IncEx | None = None,\n        exclude: IncEx | None = None,\n        context: Any | None = None,\n        by_alias: bool = False,\n        exclude_unset: bool = True,\n        exclude_defaults: bool = False,\n        exclude_none: bool = True,\n        round_trip: bool = False,\n        warnings: bool | Literal[\"none\", \"warn\", \"error\"] = True,\n        serialize_as_any: bool = False,\n    ) -> dict[str, Any]:\n        # Always exclude unset and None fields, since clients don't take it well when\n        # OAuth metadata fields are present but empty.\n        exclude_unset = True\n        exclude_none = True\n        return super().model_dump(\n            mode=mode,\n            include=include,\n            exclude=exclude,\n            context=context,\n            by_alias=by_alias,\n            exclude_unset=exclude_unset,\n            exclude_defaults=exclude_defaults,\n            exclude_none=exclude_none,\n            round_trip=round_trip,\n            warnings=warnings,\n            serialize_as_any=serialize_as_any,\n        )\n\n\nOAuthMetadataDict = Annotated[Union[Dict[str, Any], OAuthMetadata], OAuthMetadata]\n\n\nclass AuthConfig(BaseType):\n    version: Annotated[\n        Literal[\"2025-03-26\"],\n        Doc(\n            \"\"\"\n            The MCP spec version to use for setting up authorization.\n            Currently only \"2025-03-26\" is supported.\n            \"\"\"\n        ),\n    ] = \"2025-03-26\"\n\n    dependencies: Annotated[\n        Optional[Sequence[params.Depends]],\n        Doc(\n            \"\"\"\n            FastAPI dependencies (using `Depends()`) that check for authentication or authorization\n            and raise 401 or 403 errors if the request is not authenticated or authorized.\n\n            This is necessary to trigger the client to start an OAuth flow.\n\n            Example:\n            ```python\n            # Your authentication dependency\n            async def authenticate_request(request: Request, token: str = Depends(oauth2_scheme)):\n                payload = verify_token(request, token)\n                if payload is None:\n                    raise HTTPException(status_code=401, detail=\"Unauthorized\")\n                return payload\n\n            # Usage with FastAPI-MCP\n            mcp = FastApiMCP(\n                app,\n                auth_config=AuthConfig(dependencies=[Depends(authenticate_request)]),\n            )\n            ```\n            \"\"\"\n        ),\n    ] = None\n\n    issuer: Annotated[\n        Optional[str],\n        Doc(\n            \"\"\"\n            The issuer of the OAuth 2.0 server.\n            Required if you don't provide `custom_oauth_metadata`.\n            Usually it's either the base URL of your app, or the URL of the OAuth provider.\n            For example, for Auth0, this would be `https://your-tenant.auth0.com`.\n            \"\"\"\n        ),\n    ] = None\n\n    oauth_metadata_url: Annotated[\n        Optional[StrHttpUrl],\n        Doc(\n            \"\"\"\n            The full URL of the OAuth provider's metadata endpoint.\n\n            If not provided, FastAPI-MCP will attempt to guess based on the `issuer` and `metadata_path`.\n\n            Only relevant if `setup_proxies` is `True`.\n\n            If this URL is wrong, the metadata proxy will not work.\n            \"\"\"\n        ),\n    ] = None\n\n    authorize_url: Annotated[\n        Optional[StrHttpUrl],\n        Doc(\n            \"\"\"\n            The URL of your OAuth provider's authorization endpoint.\n\n            Usually this is something like `https://app.example.com/oauth/authorize`.\n            \"\"\"\n        ),\n    ] = None\n\n    audience: Annotated[\n        Optional[str],\n        Doc(\n            \"\"\"\n            Currently (2025-04-21), some Auth-supporting MCP clients (like `npx mcp-remote`) might not specify the\n            audience when sending a request to your server.\n\n            This may cause unexpected behavior from your OAuth provider, so this is a workaround.\n\n            In case the client doesn't specify an audience, this will be used as the default audience on the\n            request to your OAuth provider.\n            \"\"\"\n        ),\n    ] = None\n\n    default_scope: Annotated[\n        str,\n        Doc(\n            \"\"\"\n            Currently (2025-04-21), some Auth-supporting MCP clients (like `npx mcp-remote`) might not specify the\n            scope when sending a request to your server.\n\n            This may cause unexpected behavior from your OAuth provider, so this is a workaround.\n\n            Here is where you can optionally specify a default scope that will be sent to your OAuth provider in case\n            the client doesn't specify it.\n            \"\"\"\n        ),\n    ] = \"openid profile email\"\n\n    client_id: Annotated[\n        Optional[str],\n        Doc(\n            \"\"\"\n            In case the client doesn't specify a client ID, this will be used as the default client ID on the\n            request to your OAuth provider.\n\n            This is mandatory only if you set `setup_proxies` to `True`.\n            \"\"\"\n        ),\n    ] = None\n\n    client_secret: Annotated[\n        Optional[str],\n        Doc(\n            \"\"\"\n            The client secret to use for the client ID.\n\n            This is mandatory only if you set `setup_proxies` to `True` and `setup_fake_dynamic_registration` to `True`.\n            \"\"\"\n        ),\n    ] = None\n\n    custom_oauth_metadata: Annotated[\n        Optional[OAuthMetadataDict],\n        Doc(\n            \"\"\"\n            Custom OAuth metadata to register.\n\n            If your OAuth flow works with MCP out-of-the-box, you should just use this option to provide the\n            metadata yourself.\n\n            Otherwise, set `setup_proxies` to `True` to automatically setup MCP-compliant proxies around your\n            OAuth provider's endpoints.\n            \"\"\"\n        ),\n    ] = None\n\n    setup_proxies: Annotated[\n        bool,\n        Doc(\n            \"\"\"\n            Whether to automatically setup MCP-compliant proxies around your original OAuth provider's endpoints.\n            \"\"\"\n        ),\n    ] = False\n\n    setup_fake_dynamic_registration: Annotated[\n        bool,\n        Doc(\n            \"\"\"\n            Whether to automatically setup a fake dynamic client registration endpoint.\n\n            In MCP 2025-03-26 Spec, it is recommended to support OAuth Dynamic Client Registration (RFC 7591).\n            Furthermore, `npx mcp-remote` which is the current de-facto client that supports MCP's up-to-date spec,\n            requires this endpoint to be present.\n\n            For most cases, a fake dynamic registration endpoint is enough, thus you should set this to `True`.\n\n            This is only used if `setup_proxies` is also `True`.\n            \"\"\"\n        ),\n    ] = True\n\n    metadata_path: Annotated[\n        str,\n        Doc(\n            \"\"\"\n            The path to mount the OAuth metadata endpoint at.\n\n            Clients will usually expect this to be /.well-known/oauth-authorization-server\n            \"\"\"\n        ),\n    ] = \"/.well-known/oauth-authorization-server\"\n\n    @model_validator(mode=\"after\")\n    def validate_required_fields(self):\n        if self.custom_oauth_metadata is None and self.issuer is None and not self.dependencies:\n            raise ValueError(\"at least one of 'issuer', 'custom_oauth_metadata' or 'dependencies' is required\")\n\n        if self.setup_proxies:\n            if self.client_id is None:\n                raise ValueError(\"'client_id' is required when 'setup_proxies' is True\")\n\n            if self.setup_fake_dynamic_registration and not self.client_secret:\n                raise ValueError(\"'client_secret' is required when 'setup_fake_dynamic_registration' is True\")\n\n        return self\n\n\nclass ClientRegistrationRequest(BaseType):\n    redirect_uris: List[str]\n    client_name: Optional[str] = None\n    grant_types: Optional[List[str]] = [\"authorization_code\"]\n    token_endpoint_auth_method: Optional[str] = \"none\"\n\n\nclass ClientRegistrationResponse(BaseType):\n    client_id: str\n    client_id_issued_at: int = int(time.time())\n    client_secret: Optional[str] = None\n    client_secret_expires_at: int = 0\n    redirect_uris: List[str]\n    grant_types: List[str]\n    token_endpoint_auth_method: str\n    client_name: str\n"
  },
  {
    "path": "fastapi_mcp/utils/__init__.py",
    "content": ""
  },
  {
    "path": "mypy.ini",
    "content": "[mypy]\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nrequires = [\"hatchling\", \"tomli\"]\nbuild-backend = \"hatchling.build\"\n\n[project]\nname = \"fastapi-mcp\"\nversion = \"0.4.0\"\ndescription = \"Automatic MCP server generator for FastAPI applications - converts FastAPI endpoints to MCP tools for LLM integration\"\nreadme = \"README.md\"\nrequires-python = \">=3.10\"\nlicense = {file = \"LICENSE\"}\nauthors = [\n    {name = \"Tadata Inc.\", email = \"itay@tadata.com\"},\n]\nclassifiers = [\n    \"Development Status :: 3 - Alpha\",\n    \"Intended Audience :: Developers\",\n    \"License :: OSI Approved :: MIT License\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Topic :: Software Development :: Libraries :: Python Modules\",\n    \"Topic :: Internet :: WWW/HTTP\",\n    \"Framework :: FastAPI\",\n]\nkeywords = [\"fastapi\", \"openapi\", \"mcp\", \"llm\", \"claude\", \"ai\", \"tools\", \"api\", \"conversion\", \"modelcontextprotocol\"]\ndependencies = [\n    \"fastapi>=0.100.0\",\n    \"typer>=0.9.0\",\n    \"rich>=13.0.0\",\n    \"mcp>=1.12.0\",\n    \"pydantic>=2.0.0\",\n    \"pydantic-settings>=2.5.2\",\n    \"uvicorn>=0.20.0\",\n    \"httpx>=0.24.0\",\n    \"requests>=2.25.0\",\n    \"tomli>=2.2.1\",\n]\n\n[dependency-groups]\ndev = [\n    \"mypy>=1.15.0\",\n    \"ruff>=0.9.10\",\n    \"types-setuptools>=75.8.2.20250305\",\n    \"pytest>=8.3.5\",\n    \"pytest-asyncio>=0.26.0\",\n    \"pytest-cov>=6.1.1\",\n    \"pre-commit>=4.2.0\",\n    \"pyjwt>=2.10.1\",\n    \"cryptography>=44.0.2\",\n    \"types-jsonschema>=4.25.0.20250720\",\n]\n\n[project.urls]\nHomepage = \"https://github.com/tadata-org/fastapi_mcp\"\nDocumentation = \"https://github.com/tadata-org/fastapi_mcp#readme\"\n\"Bug Tracker\" = \"https://github.com/tadata-org/fastapi_mcp/issues\"\n\"PyPI\" = \"https://pypi.org/project/fastapi-mcp/\"\n\"Source Code\" = \"https://github.com/tadata-org/fastapi_mcp\"\n\"Changelog\" = \"https://github.com/tadata-org/fastapi_mcp/blob/main/CHANGELOG.md\"\n\n[tool.hatch.build.targets.wheel]\npackages = [\"fastapi_mcp\"]\n\n[tool.ruff]\nline-length = 120\ntarget-version = \"py312\"\n\n[tool.pytest.ini_options]\nasyncio_mode = \"auto\"\ntestpaths = [\"tests\"]\npython_files = \"test_*.py\"\n"
  },
  {
    "path": "pytest.ini",
    "content": "[pytest]\naddopts = -vvv --cov=. --cov-report xml --cov-report term-missing --cov-fail-under=80 --cov-config=.coveragerc\nasyncio_mode = auto\nlog_cli = true\nlog_cli_level = DEBUG\nlog_cli_format = %(asctime)s - %(name)s - %(levelname)s - %(message)s\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/conftest.py",
    "content": "import sys\nimport os\nimport pytest\nimport coverage\n\n# Add the parent directory to the path\nsys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), \"..\")))\n\nfrom .fixtures.types import *  # noqa: F403\nfrom .fixtures.example_data import *  # noqa: F403\nfrom .fixtures.simple_app import *  # noqa: F403\nfrom .fixtures.complex_app import *  # noqa: F403\n\n\n@pytest.hookimpl(trylast=True)\ndef pytest_configure(config):\n    \"\"\"Configure pytest-cov for proper subprocess coverage.\"\"\"\n    if config.pluginmanager.hasplugin(\"pytest_cov\"):\n        # Ensure environment variables are set for subprocess coverage\n        os.environ[\"COVERAGE_PROCESS_START\"] = os.path.abspath(\".coveragerc\")\n\n        # Set up environment for combinining coverage data from subprocesses\n        os.environ[\"PYTHONPATH\"] = os.path.abspath(\".\")\n\n        # Make sure the pytest-cov plugin is active for subprocesses\n        config.option.cov_fail_under = 0  # Disable fail under in the primary process\n\n\n@pytest.hookimpl(trylast=True)\ndef pytest_sessionfinish(session, exitstatus):\n    \"\"\"Combine coverage data from subprocesses at the end of the test session.\"\"\"\n    cov_dir = os.path.abspath(\".\")\n    if exitstatus == 0 and os.environ.get(\"COVERAGE_PROCESS_START\"):\n        try:\n            cov = coverage.Coverage()\n            cov.combine(data_paths=[cov_dir], strict=True)\n            cov.save()\n        except Exception as e:\n            print(f\"Error combining coverage data: {e}\", file=sys.stderr)\n"
  },
  {
    "path": "tests/fixtures/complex_app.py",
    "content": "from typing import Optional, List, Dict, Any, Union\nfrom uuid import UUID\n\nfrom fastapi import FastAPI, Query, Path, Body, Header, Cookie\nimport pytest\n\nfrom tests.fixtures.conftest import make_fastapi_app_base\n\nfrom .types import (\n    Product,\n    Customer,\n    OrderResponse,\n    PaginatedResponse,\n    ProductCategory,\n    OrderRequest,\n    ErrorResponse,\n)\n\n\ndef make_complex_fastapi_app(\n    example_product: Product,\n    example_customer: Customer,\n    example_order_response: OrderResponse,\n    parametrized_config: dict[str, Any] | None = None,\n) -> FastAPI:\n    app = make_fastapi_app_base(parametrized_config=parametrized_config)\n\n    @app.get(\n        \"/products\",\n        response_model=PaginatedResponse,\n        tags=[\"products\"],\n        operation_id=\"list_products\",\n        response_model_exclude_none=True,\n    )\n    async def list_products(\n        category: Optional[ProductCategory] = Query(None, description=\"Filter by product category\"),\n        min_price: Optional[float] = Query(None, description=\"Minimum price filter\", gt=0),\n        max_price: Optional[float] = Query(None, description=\"Maximum price filter\", gt=0),\n        tag: Optional[List[str]] = Query(None, description=\"Filter by tags\"),\n        sort_by: str = Query(\"created_at\", description=\"Field to sort by\"),\n        sort_direction: str = Query(\"desc\", description=\"Sort direction (asc or desc)\"),\n        in_stock_only: bool = Query(False, description=\"Show only in-stock products\"),\n        page: int = Query(1, description=\"Page number\", ge=1),\n        size: int = Query(20, description=\"Page size\", ge=1, le=100),\n        user_agent: Optional[str] = Header(None, description=\"User agent header\"),\n    ):\n        \"\"\"\n        List products with various filtering, sorting and pagination options.\n        Returns a paginated response of products.\n        \"\"\"\n        return PaginatedResponse(items=[example_product], total=1, page=page, size=size, pages=1)\n\n    @app.get(\n        \"/products/{product_id}\",\n        response_model=Product,\n        tags=[\"products\"],\n        operation_id=\"get_product\",\n        responses={\n            404: {\"model\": ErrorResponse, \"description\": \"Product not found\"},\n        },\n    )\n    async def get_product(\n        product_id: UUID = Path(..., description=\"The ID of the product to retrieve\"),\n        include_unavailable: bool = Query(False, description=\"Include product even if not available\"),\n    ):\n        \"\"\"\n        Get detailed information about a specific product by its ID.\n        Includes all variants, images, and metadata.\n        \"\"\"\n        # Just returning the example product with the requested ID\n        product_copy = example_product.model_copy()\n        product_copy.id = product_id\n        return product_copy\n\n    @app.post(\n        \"/orders\",\n        response_model=OrderResponse,\n        tags=[\"orders\"],\n        operation_id=\"create_order\",\n        status_code=201,\n        responses={\n            400: {\"model\": ErrorResponse, \"description\": \"Invalid order data\"},\n            404: {\"model\": ErrorResponse, \"description\": \"Customer or product not found\"},\n            422: {\"model\": ErrorResponse, \"description\": \"Validation error\"},\n        },\n    )\n    async def create_order(\n        order: OrderRequest = Body(..., description=\"Order details\"),\n        user_id: Optional[UUID] = Cookie(None, description=\"User ID from cookie\"),\n        authorization: Optional[str] = Header(None, description=\"Authorization header\"),\n    ):\n        \"\"\"\n        Create a new order with multiple items, shipping details, and payment information.\n        Returns the created order with full details including status and tracking information.\n        \"\"\"\n        # Return a copy of the example order response with the customer ID from the request\n        order_copy = example_order_response.model_copy()\n        order_copy.customer_id = order.customer_id\n        order_copy.items = order.items\n        return order_copy\n\n    @app.get(\n        \"/customers/{customer_id}\",\n        response_model=Union[Customer, Dict[str, Any]],\n        tags=[\"customers\"],\n        operation_id=\"get_customer\",\n        responses={\n            404: {\"model\": ErrorResponse, \"description\": \"Customer not found\"},\n            403: {\"model\": ErrorResponse, \"description\": \"Forbidden access\"},\n        },\n    )\n    async def get_customer(\n        customer_id: UUID = Path(..., description=\"The ID of the customer to retrieve\"),\n        include_orders: bool = Query(False, description=\"Include customer's order history\"),\n        include_payment_methods: bool = Query(False, description=\"Include customer's saved payment methods\"),\n        fields: List[str] = Query(None, description=\"Specific fields to include in response\"),\n    ):\n        \"\"\"\n        Get detailed information about a specific customer by ID.\n        Can include additional related information like order history.\n        \"\"\"\n        # Return a copy of the example customer with the requested ID\n        customer_copy = example_customer.model_copy()\n        customer_copy.id = customer_id\n        return customer_copy\n\n    return app\n\n\n@pytest.fixture\ndef complex_fastapi_app(\n    example_product: Product,\n    example_customer: Customer,\n    example_order_response: OrderResponse,\n) -> FastAPI:\n    return make_complex_fastapi_app(\n        example_product=example_product,\n        example_customer=example_customer,\n        example_order_response=example_order_response,\n    )\n"
  },
  {
    "path": "tests/fixtures/conftest.py",
    "content": "from typing import Any\nfrom fastapi import FastAPI\n\n\ndef make_fastapi_app_base(parametrized_config: dict[str, Any] | None = None) -> FastAPI:\n    fastapi_config: dict[str, Any] = {\n        \"title\": \"Test API\",\n        \"description\": \"A test API app for unit testing\",\n        \"version\": \"0.1.0\",\n    }\n    app = FastAPI(**fastapi_config | parametrized_config if parametrized_config is not None else {})\n    return app\n"
  },
  {
    "path": "tests/fixtures/example_data.py",
    "content": "from datetime import datetime, date\nfrom uuid import UUID\n\nimport pytest\n\nfrom .types import (\n    Address,\n    ProductVariant,\n    Product,\n    ProductCategory,\n    Customer,\n    CustomerTier,\n    OrderItem,\n    PaymentDetails,\n    OrderRequest,\n    OrderResponse,\n    PaginatedResponse,\n    OrderStatus,\n    PaymentMethod,\n)\n\n\n@pytest.fixture\ndef example_address() -> Address:\n    return Address(street=\"123 Main St\", city=\"Anytown\", state=\"CA\", postal_code=\"12345\", country=\"US\", is_primary=True)\n\n\n@pytest.fixture\ndef example_product_variant() -> ProductVariant:\n    return ProductVariant(\n        sku=\"EP-001-BLK\", color=\"Black\", stock_count=10, size=None, weight=None, dimensions=None, in_stock=True\n    )\n\n\n@pytest.fixture\ndef example_product(example_product_variant) -> Product:\n    return Product(\n        id=UUID(\"550e8400-e29b-41d4-a716-446655440000\"),\n        name=\"Example Product\",\n        description=\"This is an example product\",\n        category=ProductCategory.ELECTRONICS,\n        price=199.99,\n        discount_percent=None,\n        tax_rate=None,\n        rating=None,\n        review_count=0,\n        tags=[\"example\", \"new\"],\n        image_urls=[\"https://example.com/image.jpg\"],\n        created_at=datetime.now(),\n        variants=[example_product_variant],\n    )\n\n\n@pytest.fixture\ndef example_customer(example_address) -> Customer:\n    return Customer(\n        id=UUID(\"770f9511-f39c-42d5-a860-557654551222\"),\n        email=\"customer@example.com\",\n        full_name=\"John Doe\",\n        phone=\"1234567890\",\n        tier=CustomerTier.STANDARD,\n        addresses=[example_address],\n        created_at=datetime.now(),\n        preferences={\"theme\": \"dark\", \"notifications\": True},\n        consent={\"marketing\": True, \"analytics\": True},\n    )\n\n\n@pytest.fixture\ndef example_order_item() -> OrderItem:\n    return OrderItem(\n        product_id=UUID(\"550e8400-e29b-41d4-a716-446655440000\"),\n        variant_sku=\"EP-001-BLK\",\n        quantity=2,\n        unit_price=199.99,\n        discount_amount=10.00,\n        total=389.98,\n    )\n\n\n@pytest.fixture\ndef example_payment_details() -> PaymentDetails:\n    return PaymentDetails(\n        method=PaymentMethod.CREDIT_CARD,\n        transaction_id=\"txn_12345\",\n        status=\"completed\",\n        amount=389.98,\n        currency=\"USD\",\n        paid_at=datetime.now(),\n    )\n\n\n@pytest.fixture\ndef example_order_request(example_order_item) -> OrderRequest:\n    return OrderRequest(\n        customer_id=UUID(\"770f9511-f39c-42d5-a860-557654551222\"),\n        items=[example_order_item],\n        shipping_address_id=UUID(\"880f9511-f39c-42d5-a860-557654551333\"),\n        billing_address_id=None,\n        payment_method=PaymentMethod.CREDIT_CARD,\n        notes=\"Please deliver before 6pm\",\n        use_loyalty_points=False,\n    )\n\n\n@pytest.fixture\ndef example_order_response(example_order_item, example_address, example_payment_details) -> OrderResponse:\n    return OrderResponse(\n        id=UUID(\"660f9511-f39c-42d5-a860-557654551111\"),\n        customer_id=UUID(\"770f9511-f39c-42d5-a860-557654551222\"),\n        status=OrderStatus.PENDING,\n        items=[example_order_item],\n        shipping_address=example_address,\n        billing_address=example_address,\n        payment=example_payment_details,\n        subtotal=389.98,\n        shipping_cost=10.0,\n        tax_amount=20.0,\n        discount_amount=10.0,\n        total_amount=409.98,\n        tracking_number=\"TRK123456789\",\n        estimated_delivery=date.today(),\n        created_at=datetime.now(),\n        notes=\"Please deliver before 6pm\",\n        metadata={},\n    )\n\n\n@pytest.fixture\ndef example_paginated_products(example_product) -> PaginatedResponse:\n    return PaginatedResponse(items=[example_product], total=1, page=1, size=20, pages=1)\n"
  },
  {
    "path": "tests/fixtures/simple_app.py",
    "content": "from typing import Optional, List, Any\n\nfrom fastapi import FastAPI, Query, Path, Body, HTTPException\nimport pytest\n\nfrom tests.fixtures.conftest import make_fastapi_app_base\n\nfrom .types import Item\n\n\ndef make_simple_fastapi_app(parametrized_config: dict[str, Any] | None = None) -> FastAPI:\n    app = make_fastapi_app_base(parametrized_config=parametrized_config)\n    items = [\n        Item(id=1, name=\"Item 1\", price=10.0, tags=[\"tag1\", \"tag2\"], description=\"Item 1 description\"),\n        Item(id=2, name=\"Item 2\", price=20.0, tags=[\"tag2\", \"tag3\"]),\n        Item(id=3, name=\"Item 3\", price=30.0, tags=[\"tag3\", \"tag4\"], description=\"Item 3 description\"),\n    ]\n\n    @app.get(\"/items/\", response_model=List[Item], tags=[\"items\"], operation_id=\"list_items\")\n    async def list_items(\n        skip: int = Query(0, description=\"Number of items to skip\"),\n        limit: int = Query(10, description=\"Max number of items to return\"),\n        sort_by: Optional[str] = Query(None, description=\"Field to sort by\"),\n    ) -> List[Item]:\n        \"\"\"List all items with pagination and sorting options.\"\"\"\n        return items[skip : skip + limit]\n\n    @app.get(\"/items/{item_id}\", response_model=Item, tags=[\"items\"], operation_id=\"get_item\")\n    async def read_item(\n        item_id: int = Path(..., description=\"The ID of the item to retrieve\"),\n        include_details: bool = Query(False, description=\"Include additional details\"),\n    ) -> Item:\n        \"\"\"Get a specific item by its ID with optional details.\"\"\"\n        found_item = next((item for item in items if item.id == item_id), None)\n        if found_item is None:\n            raise HTTPException(status_code=404, detail=\"Item not found\")\n        return found_item\n\n    @app.post(\"/items/\", response_model=Item, tags=[\"items\"], operation_id=\"create_item\")\n    async def create_item(item: Item = Body(..., description=\"The item to create\")) -> Item:\n        \"\"\"Create a new item in the database.\"\"\"\n        items.append(item)\n        return item\n\n    @app.put(\"/items/{item_id}\", response_model=Item, tags=[\"items\"], operation_id=\"update_item\")\n    async def update_item(\n        item_id: int = Path(..., description=\"The ID of the item to update\"),\n        item: Item = Body(..., description=\"The updated item data\"),\n    ) -> Item:\n        \"\"\"Update an existing item.\"\"\"\n        item.id = item_id\n        return item\n\n    @app.delete(\"/items/{item_id}\", status_code=204, tags=[\"items\"], operation_id=\"delete_item\")\n    async def delete_item(item_id: int = Path(..., description=\"The ID of the item to delete\")) -> None:\n        \"\"\"Delete an item from the database.\"\"\"\n        return None\n\n    @app.get(\"/error\", tags=[\"error\"], operation_id=\"raise_error\")\n    async def raise_error() -> None:\n        \"\"\"Fail on purpose and cause a 500 error.\"\"\"\n        raise Exception(\"This is a test error\")\n\n    return app\n\n\n@pytest.fixture\ndef simple_fastapi_app() -> FastAPI:\n    return make_simple_fastapi_app()\n\n\n@pytest.fixture\ndef simple_fastapi_app_with_root_path() -> FastAPI:\n    return make_simple_fastapi_app(parametrized_config={\"root_path\": \"/api/v1\"})\n"
  },
  {
    "path": "tests/fixtures/types.py",
    "content": "from typing import Optional, List, Dict, Any\nfrom datetime import datetime, date\nfrom enum import Enum\nfrom uuid import UUID\n\nfrom pydantic import BaseModel, Field\n\n\nclass Item(BaseModel):\n    id: int\n    name: str\n    description: Optional[str] = None\n    price: float\n    tags: List[str] = []\n\n\nclass OrderStatus(str, Enum):\n    PENDING = \"pending\"\n    PROCESSING = \"processing\"\n    SHIPPED = \"shipped\"\n    DELIVERED = \"delivered\"\n    CANCELLED = \"cancelled\"\n    RETURNED = \"returned\"\n\n\nclass PaymentMethod(str, Enum):\n    CREDIT_CARD = \"credit_card\"\n    DEBIT_CARD = \"debit_card\"\n    PAYPAL = \"paypal\"\n    BANK_TRANSFER = \"bank_transfer\"\n    CASH_ON_DELIVERY = \"cash_on_delivery\"\n\n\nclass ProductCategory(str, Enum):\n    ELECTRONICS = \"electronics\"\n    CLOTHING = \"clothing\"\n    FOOD = \"food\"\n    BOOKS = \"books\"\n    OTHER = \"other\"\n\n\nclass ProductVariant(BaseModel):\n    sku: str = Field(..., description=\"Stock keeping unit code\")\n    color: Optional[str] = Field(None, description=\"Color variant\")\n    size: Optional[str] = Field(None, description=\"Size variant\")\n    weight: Optional[float] = Field(None, description=\"Weight in kg\", gt=0)\n    dimensions: Optional[Dict[str, float]] = Field(None, description=\"Dimensions in cm (length, width, height)\")\n    in_stock: bool = Field(True, description=\"Whether this variant is in stock\")\n    stock_count: Optional[int] = Field(None, description=\"Number of items in stock\", ge=0)\n\n\nclass Address(BaseModel):\n    street: str\n    city: str\n    state: str\n    postal_code: str\n    country: str\n    is_primary: bool = False\n\n\nclass CustomerTier(str, Enum):\n    STANDARD = \"standard\"\n    PREMIUM = \"premium\"\n    VIP = \"vip\"\n\n\nclass Customer(BaseModel):\n    id: UUID\n    email: str\n    full_name: str\n    phone: Optional[str] = Field(None, min_length=10, max_length=15)\n    tier: CustomerTier = CustomerTier.STANDARD\n    addresses: List[Address] = []\n    is_active: bool = True\n    created_at: datetime\n    last_login: Optional[datetime] = None\n    preferences: Dict[str, Any] = {}\n    consent: Dict[str, bool] = {}\n\n\nclass Product(BaseModel):\n    id: UUID\n    name: str\n    description: str\n    category: ProductCategory\n    price: float = Field(..., gt=0)\n    discount_percent: Optional[float] = Field(None, ge=0, le=100)\n    tax_rate: Optional[float] = Field(None, ge=0, le=100)\n    variants: List[ProductVariant] = []\n    tags: List[str] = []\n    image_urls: List[str] = []\n    rating: Optional[float] = Field(None, ge=0, le=5)\n    review_count: int = Field(0, ge=0)\n    created_at: datetime\n    updated_at: Optional[datetime] = None\n    is_available: bool = True\n    metadata: Dict[str, Any] = {}\n\n\nclass OrderItem(BaseModel):\n    product_id: UUID\n    variant_sku: Optional[str] = None\n    quantity: int = Field(..., gt=0)\n    unit_price: float\n    discount_amount: float = 0\n    total: float\n\n\nclass PaymentDetails(BaseModel):\n    method: PaymentMethod\n    transaction_id: Optional[str] = None\n    status: str\n    amount: float\n    currency: str = \"USD\"\n    paid_at: Optional[datetime] = None\n\n\nclass OrderRequest(BaseModel):\n    customer_id: UUID\n    items: List[OrderItem]\n    shipping_address_id: UUID\n    billing_address_id: Optional[UUID] = None\n    payment_method: PaymentMethod\n    notes: Optional[str] = None\n    use_loyalty_points: bool = False\n\n\nclass OrderResponse(BaseModel):\n    id: UUID\n    customer_id: UUID\n    status: OrderStatus = OrderStatus.PENDING\n    items: List[OrderItem]\n    shipping_address: Address\n    billing_address: Address\n    payment: PaymentDetails\n    subtotal: float\n    shipping_cost: float\n    tax_amount: float\n    discount_amount: float\n    total_amount: float\n    tracking_number: Optional[str] = None\n    estimated_delivery: Optional[date] = None\n    created_at: datetime\n    updated_at: Optional[datetime] = None\n    notes: Optional[str] = None\n    metadata: Dict[str, Any] = {}\n\n\nclass PaginatedResponse(BaseModel):\n    items: List[Any]\n    total: int\n    page: int\n    size: int\n    pages: int\n\n\nclass ErrorResponse(BaseModel):\n    status_code: int\n    message: str\n    details: Optional[Dict[str, Any]] = None\n"
  },
  {
    "path": "tests/test_basic_functionality.py",
    "content": "from fastapi import FastAPI\nfrom mcp.server.lowlevel.server import Server\n\nfrom fastapi_mcp import FastApiMCP\n\n\ndef test_create_mcp_server(simple_fastapi_app: FastAPI):\n    \"\"\"Test creating an MCP server without mounting it.\"\"\"\n    mcp = FastApiMCP(\n        simple_fastapi_app,\n        name=\"Test MCP Server\",\n        description=\"Test description\",\n    )\n\n    # Verify the MCP server was created correctly\n    assert mcp.name == \"Test MCP Server\"\n    assert mcp.description == \"Test description\"\n    assert isinstance(mcp.server, Server)\n    assert len(mcp.tools) > 0, \"Should have extracted tools from the app\"\n    assert len(mcp.operation_map) > 0, \"Should have operation mapping\"\n\n    # Check that the operation map contains all expected operations from simple_app\n    expected_operations = [\"list_items\", \"get_item\", \"create_item\", \"update_item\", \"delete_item\", \"raise_error\"]\n    for op in expected_operations:\n        assert op in mcp.operation_map, f\"Operation {op} not found in operation map\"\n\n\ndef test_default_values(simple_fastapi_app: FastAPI):\n    \"\"\"Test that default values are used when not explicitly provided.\"\"\"\n    mcp = FastApiMCP(simple_fastapi_app)\n\n    # Verify default values\n    assert mcp.name == simple_fastapi_app.title\n    assert mcp.description == simple_fastapi_app.description\n\n    # Mount with default path\n    mcp.mount()\n\n    # Check that the MCP server was properly mounted\n    # Look for a route that includes our mount path in its pattern\n    route_found = any(\"/mcp\" in str(route) for route in simple_fastapi_app.routes)\n    assert route_found, \"MCP server mount point not found in app routes\"\n\n\ndef test_normalize_paths(simple_fastapi_app: FastAPI):\n    \"\"\"Test that mount paths are normalized correctly.\"\"\"\n    mcp = FastApiMCP(simple_fastapi_app)\n\n    # Test with path without leading slash\n    mount_path = \"test-mcp\"\n    mcp.mount(mount_path=mount_path)\n\n    # Check that the route was added with a normalized path\n    route_found = any(\"/test-mcp\" in str(route) for route in simple_fastapi_app.routes)\n    assert route_found, \"Normalized mount path not found in app routes\"\n\n    # Create a new MCP server\n    mcp2 = FastApiMCP(simple_fastapi_app)\n\n    # Test with path with trailing slash\n    mount_path = \"/test-mcp2/\"\n    mcp2.mount(mount_path=mount_path)\n\n    # Check that the route was added with a normalized path\n    route_found = any(\"/test-mcp2\" in str(route) for route in simple_fastapi_app.routes)\n    assert route_found, \"Normalized mount path not found in app routes\"\n"
  },
  {
    "path": "tests/test_configuration.py",
    "content": "from fastapi import FastAPI\nimport pytest\n\nfrom fastapi_mcp import FastApiMCP\n\n\ndef test_default_configuration(simple_fastapi_app: FastAPI):\n    \"\"\"Test the default configuration of FastApiMCP.\"\"\"\n    # Create MCP server with defaults\n    mcp_server = FastApiMCP(simple_fastapi_app)\n\n    # Check default name and description\n    assert mcp_server.name == simple_fastapi_app.title\n    assert mcp_server.description == simple_fastapi_app.description\n\n    # Check default options\n    assert mcp_server._describe_all_responses is False\n    assert mcp_server._describe_full_response_schema is False\n\n\ndef test_custom_configuration(simple_fastapi_app: FastAPI):\n    \"\"\"Test a custom configuration of FastApiMCP.\"\"\"\n    # Create MCP server with custom options\n    custom_name = \"Custom MCP Server\"\n    custom_description = \"A custom MCP server for testing\"\n\n    mcp_server = FastApiMCP(\n        simple_fastapi_app,\n        name=custom_name,\n        description=custom_description,\n        describe_all_responses=True,\n        describe_full_response_schema=True,\n    )\n\n    # Check custom name and description\n    assert mcp_server.name == custom_name\n    assert mcp_server.description == custom_description\n\n    # Check custom options\n    assert mcp_server._describe_all_responses is True\n    assert mcp_server._describe_full_response_schema is True\n\n\ndef test_describe_all_responses_config_simple_app(simple_fastapi_app: FastAPI):\n    \"\"\"Test the describe_all_responses behavior with the simple app.\"\"\"\n    mcp_default = FastApiMCP(\n        simple_fastapi_app,\n    )\n\n    mcp_all_responses = FastApiMCP(\n        simple_fastapi_app,\n        describe_all_responses=True,\n    )\n\n    for tool in mcp_default.tools:\n        assert tool.description is not None\n        if tool.name == \"raise_error\":\n            pass\n        elif tool.name != \"delete_item\":\n            assert tool.description.count(\"**200**\") == 1, \"The description should contain a 200 status code\"\n            assert tool.description.count(\"**Example Response:**\") == 1, (\n                \"The description should only contain one example response\"\n            )\n            assert tool.description.count(\"**Output Schema:**\") == 0, (\n                \"The description should not contain a full output schema\"\n            )\n        else:\n            # The delete endpoint in the Items API returns a 204 status code\n            assert tool.description.count(\"**200**\") == 0, \"The description should not contain a 200 status code\"\n            assert tool.description.count(\"**204**\") == 1, \"The description should contain a 204 status code\"\n            # The delete endpoint in the Items API returns a 204 status code and has no response body\n            assert tool.description.count(\"**Example Response:**\") == 0, (\n                \"The description should not contain any example responses\"\n            )\n            assert tool.description.count(\"**Output Schema:**\") == 0, (\n                \"The description should not contain a full output schema\"\n            )\n\n    for tool in mcp_all_responses.tools:\n        assert tool.description is not None\n        if tool.name == \"raise_error\":\n            pass\n        elif tool.name != \"delete_item\":\n            assert tool.description.count(\"**200**\") == 1, \"The description should contain a 200 status code\"\n            assert tool.description.count(\"**422**\") == 1, \"The description should contain a 422 status code\"\n            assert tool.description.count(\"**Example Response:**\") == 2, (\n                \"The description should contain two example responses\"\n            )\n            assert tool.description.count(\"**Output Schema:**\") == 0, (\n                \"The description should not contain a full output schema\"\n            )\n        else:\n            assert tool.description.count(\"**204**\") == 1, \"The description should contain a 204 status code\"\n            assert tool.description.count(\"**422**\") == 1, \"The description should contain a 422 status code\"\n            # The delete endpoint in the Items API returns a 204 status code and has no response body\n            # But FastAPI's default 422 response should be present\n            # So just 1 instance of Example Response should be present\n            assert tool.description.count(\"**Example Response:**\") == 1, (\n                \"The description should contain one example response\"\n            )\n            assert tool.description.count(\"**Output Schema:**\") == 0, (\n                \"The description should not contain any output schema\"\n            )\n\n\ndef test_describe_full_response_schema_config_simple_app(simple_fastapi_app: FastAPI):\n    \"\"\"Test the describe_full_response_schema behavior with the simple app.\"\"\"\n\n    mcp_full_response_schema = FastApiMCP(\n        simple_fastapi_app,\n        describe_full_response_schema=True,\n    )\n\n    for tool in mcp_full_response_schema.tools:\n        assert tool.description is not None\n        if tool.name == \"raise_error\":\n            pass\n        elif tool.name != \"delete_item\":\n            assert tool.description.count(\"**200**\") == 1, \"The description should contain a 200 status code\"\n            assert tool.description.count(\"**Example Response:**\") == 1, (\n                \"The description should only contain one example response\"\n            )\n            assert tool.description.count(\"**Output Schema:**\") == 1, (\n                \"The description should contain one full output schema\"\n            )\n        else:\n            # The delete endpoint in the Items API returns a 204 status code\n            assert tool.description.count(\"**200**\") == 0, \"The description should not contain a 200 status code\"\n            assert tool.description.count(\"**204**\") == 1, \"The description should contain a 204 status code\"\n            # The delete endpoint in the Items API returns a 204 status code and has no response body\n            assert tool.description.count(\"**Example Response:**\") == 0, (\n                \"The description should not contain any example responses\"\n            )\n            assert tool.description.count(\"**Output Schema:**\") == 0, (\n                \"The description should not contain a full output schema\"\n            )\n\n\ndef test_describe_all_responses_and_full_response_schema_config_simple_app(simple_fastapi_app: FastAPI):\n    \"\"\"Test the describe_all_responses and describe_full_response_schema params together with the simple app.\"\"\"\n\n    mcp_all_responses_and_full_response_schema = FastApiMCP(\n        simple_fastapi_app,\n        describe_all_responses=True,\n        describe_full_response_schema=True,\n    )\n\n    for tool in mcp_all_responses_and_full_response_schema.tools:\n        assert tool.description is not None\n        if tool.name == \"raise_error\":\n            pass\n        elif tool.name != \"delete_item\":\n            assert tool.description.count(\"**200**\") == 1, \"The description should contain a 200 status code\"\n            assert tool.description.count(\"**422**\") == 1, \"The description should contain a 422 status code\"\n            assert tool.description.count(\"**Example Response:**\") == 2, (\n                \"The description should contain two example responses\"\n            )\n            assert tool.description.count(\"**Output Schema:**\") == 2, (\n                \"The description should contain two full output schemas\"\n            )\n        else:\n            # The delete endpoint in the Items API returns a 204 status code\n            assert tool.description.count(\"**200**\") == 0, \"The description should not contain a 200 status code\"\n            assert tool.description.count(\"**204**\") == 1, \"The description should contain a 204 status code\"\n            assert tool.description.count(\"**422**\") == 1, \"The description should contain a 422 status code\"\n            # The delete endpoint in the Items API returns a 204 status code and has no response body\n            # But FastAPI's default 422 response should be present\n            # So just 1 instance of Example Response and Output Schema should be present\n            assert tool.description.count(\"**Example Response:**\") == 1, (\n                \"The description should contain one example response\"\n            )\n            assert tool.description.count(\"**Output Schema:**\") == 1, (\n                \"The description should contain one full output schema\"\n            )\n\n\ndef test_describe_all_responses_config_complex_app(complex_fastapi_app: FastAPI):\n    \"\"\"Test the describe_all_responses behavior with the complex app.\"\"\"\n    mcp_default = FastApiMCP(\n        complex_fastapi_app,\n    )\n\n    mcp_all_responses = FastApiMCP(\n        complex_fastapi_app,\n        describe_all_responses=True,\n    )\n\n    # Test default behavior (only success responses)\n    for tool in mcp_default.tools:\n        assert tool.description is not None\n\n        # Check get_product which has a 200 response and 404 error response defined\n        if tool.name == \"get_product\":\n            assert tool.description.count(\"**200**\") == 1, \"The description should contain a 200 status code\"\n            assert tool.description.count(\"**404**\") == 0, \"The description should not contain a 404 status code\"\n            # Some endpoints might not have example responses if they couldn't be generated\n            # Only verify no error responses are included\n\n        # Check create_order which has 201, 400, 404, and 422 responses defined\n        elif tool.name == \"create_order\":\n            assert tool.description.count(\"**201**\") == 1, \"The description should contain a 201 status code\"\n            assert tool.description.count(\"**400**\") == 0, \"The description should not contain a 400 status code\"\n            assert tool.description.count(\"**404**\") == 0, \"The description should not contain a 404 status code\"\n            assert tool.description.count(\"**422**\") == 0, \"The description should not contain a 422 status code\"\n            # Some endpoints might not have example responses if they couldn't be generated\n\n        # Check get_customer which has 200, 404, and 403 responses defined\n        elif tool.name == \"get_customer\":\n            assert tool.description.count(\"**200**\") == 1, \"The description should contain a 200 status code\"\n            assert tool.description.count(\"**404**\") == 0, \"The description should not contain a 404 status code\"\n            assert tool.description.count(\"**403**\") == 0, \"The description should not contain a 403 status code\"\n            # Based on the error message, this endpoint doesn't have example responses in the description\n            assert tool.description.count(\"**Example Response:**\") == 0, (\n                \"This endpoint doesn't appear to have example responses in the default configuration\"\n            )\n            assert tool.description.count(\"**Output Schema:**\") == 0, (\n                \"The description should not contain a full output schema\"\n            )\n\n    # Test with describe_all_responses=True (should include error responses)\n    for tool in mcp_all_responses.tools:\n        assert tool.description is not None\n\n        # Check get_product which has a 200 response and 404 error response defined\n        if tool.name == \"get_product\":\n            assert tool.description.count(\"**200**\") == 1, \"The description should contain a 200 status code\"\n            assert tool.description.count(\"**404**\") == 1, \"The description should contain a 404 status code\"\n            assert tool.description.count(\"**422**\") == 1, \"The description should contain a 422 status code\"\n            # Don't check exact count as implementations may vary, just ensure there are examples\n\n        # Check create_order which has 201, 400, 404, and 422 responses defined\n        elif tool.name == \"create_order\":\n            assert tool.description.count(\"**201**\") == 1, \"The description should contain a 201 status code\"\n            assert tool.description.count(\"**400**\") == 1, \"The description should contain a 400 status code\"\n            assert tool.description.count(\"**404**\") == 1, \"The description should contain a 404 status code\"\n            assert tool.description.count(\"**422**\") == 1, \"The description should contain a 422 status code\"\n            # Don't check exact count as implementations may vary, just ensure there are examples\n\n        # Check get_customer which has 200, 404, and 403 responses defined\n        elif tool.name == \"get_customer\":\n            assert tool.description.count(\"**200**\") == 1, \"The description should contain a 200 status code\"\n            assert tool.description.count(\"**404**\") == 1, \"The description should contain a 404 status code\"\n            assert tool.description.count(\"**403**\") == 1, \"The description should contain a 403 status code\"\n            assert tool.description.count(\"**422**\") == 1, \"The description should contain a 422 status code\"\n            # Based on error messages, we need to check actual implementation behavior\n\n\ndef test_describe_full_response_schema_config_complex_app(complex_fastapi_app: FastAPI):\n    \"\"\"Test the describe_full_response_schema behavior with the complex app.\"\"\"\n    mcp_full_response_schema = FastApiMCP(\n        complex_fastapi_app,\n        describe_full_response_schema=True,\n    )\n\n    for tool in mcp_full_response_schema.tools:\n        assert tool.description is not None\n\n        # Check get_product which has a 200 response and 404 error response defined\n        if tool.name == \"get_product\":\n            assert tool.description.count(\"**200**\") == 1, \"The description should contain a 200 status code\"\n            assert tool.description.count(\"**404**\") == 0, \"The description should not contain a 404 status code\"\n            # Only verify the success response schema is present\n            assert tool.description.count(\"**Output Schema:**\") >= 1, (\n                \"The description should contain at least one full output schema\"\n            )\n\n        # Check create_order which has 201, 400, 404, and 422 responses defined\n        elif tool.name == \"create_order\":\n            assert tool.description.count(\"**201**\") == 1, \"The description should contain a 201 status code\"\n            assert tool.description.count(\"**400**\") == 0, \"The description should not contain a 400 status code\"\n            assert tool.description.count(\"**404**\") == 0, \"The description should not contain a 404 status code\"\n            assert tool.description.count(\"**422**\") == 0, \"The description should not contain a 422 status code\"\n            # Only verify the success response schema is present\n            assert tool.description.count(\"**Output Schema:**\") >= 1, (\n                \"The description should contain at least one full output schema\"\n            )\n\n        # Check get_customer which has 200, 404, and 403 responses defined\n        elif tool.name == \"get_customer\":\n            assert tool.description.count(\"**200**\") == 1, \"The description should contain a 200 status code\"\n            assert tool.description.count(\"**404**\") == 0, \"The description should not contain a 404 status code\"\n            assert tool.description.count(\"**403**\") == 0, \"The description should not contain a 403 status code\"\n            # Based on error message, there are no example responses but there is an output schema\n            assert tool.description.count(\"**Example Response:**\") == 0, (\n                \"This endpoint doesn't appear to have example responses\"\n            )\n            assert tool.description.count(\"**Output Schema:**\") >= 1, (\n                \"The description should contain at least one full output schema\"\n            )\n\n\ndef test_describe_all_responses_and_full_response_schema_config_complex_app(complex_fastapi_app: FastAPI):\n    \"\"\"Test the describe_all_responses and describe_full_response_schema together with the complex app.\"\"\"\n    mcp_all_responses_and_full_schema = FastApiMCP(\n        complex_fastapi_app,\n        describe_all_responses=True,\n        describe_full_response_schema=True,\n    )\n\n    for tool in mcp_all_responses_and_full_schema.tools:\n        assert tool.description is not None\n\n        # Check get_product which has a 200 response and 404 error response defined\n        if tool.name == \"get_product\":\n            assert tool.description.count(\"**200**\") == 1, \"The description should contain a 200 status code\"\n            assert tool.description.count(\"**404**\") == 1, \"The description should contain a 404 status code\"\n            assert tool.description.count(\"**422**\") == 1, \"The description should contain a 422 status code\"\n            # Based on the error message data, adjust the expected counts\n            # Don't check exact counts, just ensure they exist\n            assert tool.description.count(\"**Example Response:**\") > 0, (\n                \"The description should contain example responses\"\n            )\n            assert tool.description.count(\"**Output Schema:**\") > 0, (\n                \"The description should contain full output schemas\"\n            )\n\n        # Check create_order which has 201, 400, 404, and 422 responses defined\n        elif tool.name == \"create_order\":\n            assert tool.description.count(\"**201**\") == 1, \"The description should contain a 201 status code\"\n            assert tool.description.count(\"**400**\") == 1, \"The description should contain a 400 status code\"\n            assert tool.description.count(\"**404**\") == 1, \"The description should contain a 404 status code\"\n            assert tool.description.count(\"**422**\") == 1, \"The description should contain a 422 status code\"\n            # Don't check exact counts, just ensure they exist\n            assert tool.description.count(\"**Example Response:**\") > 0, (\n                \"The description should contain example responses\"\n            )\n            assert tool.description.count(\"**Output Schema:**\") > 0, (\n                \"The description should contain full output schemas\"\n            )\n\n        # Check get_customer which has 200, 404, and 403 responses defined\n        elif tool.name == \"get_customer\":\n            assert tool.description.count(\"**200**\") == 1, \"The description should contain a 200 status code\"\n            assert tool.description.count(\"**404**\") == 1, \"The description should contain a 404 status code\"\n            assert tool.description.count(\"**403**\") == 1, \"The description should contain a 403 status code\"\n            assert tool.description.count(\"**422**\") == 1, \"The description should contain a 422 status code\"\n            # From error message, we know there are exactly 3 example responses for this endpoint\n            assert tool.description.count(\"**Example Response:**\") == 3, (\n                \"The description should contain exactly three example responses\"\n            )\n            assert tool.description.count(\"**Output Schema:**\") > 0, (\n                \"The description should contain full output schemas\"\n            )\n\n\ndef test_filtering_functionality():\n    \"\"\"Test that FastApiMCP correctly filters endpoints based on operation IDs and tags.\"\"\"\n    app = FastAPI()\n\n    # Define endpoints with different operation IDs and tags\n    @app.get(\"/items/\", operation_id=\"list_items\", tags=[\"items\"])\n    async def list_items():\n        return [{\"id\": 1}]\n\n    @app.get(\"/items/{item_id}\", operation_id=\"get_item\", tags=[\"items\", \"read\"])\n    async def get_item(item_id: int):\n        return {\"id\": item_id}\n\n    @app.post(\"/items/\", operation_id=\"create_item\", tags=[\"items\", \"write\"])\n    async def create_item():\n        return {\"id\": 2}\n\n    @app.put(\"/items/{item_id}\", operation_id=\"update_item\", tags=[\"items\", \"write\"])\n    async def update_item(item_id: int):\n        return {\"id\": item_id}\n\n    @app.delete(\"/items/{item_id}\", operation_id=\"delete_item\", tags=[\"items\", \"delete\"])\n    async def delete_item(item_id: int):\n        return {\"id\": item_id}\n\n    @app.get(\"/search/\", operation_id=\"search_items\", tags=[\"search\"])\n    async def search_items():\n        return [{\"id\": 1}]\n\n    # Test include_operations\n    include_ops_mcp = FastApiMCP(app, include_operations=[\"get_item\", \"list_items\"])\n    assert len(include_ops_mcp.tools) == 2\n    assert {tool.name for tool in include_ops_mcp.tools} == {\"get_item\", \"list_items\"}\n\n    # Test exclude_operations\n    exclude_ops_mcp = FastApiMCP(app, exclude_operations=[\"delete_item\", \"search_items\"])\n    assert len(exclude_ops_mcp.tools) == 4\n    assert {tool.name for tool in exclude_ops_mcp.tools} == {\"get_item\", \"list_items\", \"create_item\", \"update_item\"}\n\n    # Test include_tags\n    include_tags_mcp = FastApiMCP(app, include_tags=[\"read\"])\n    assert len(include_tags_mcp.tools) == 1\n    assert {tool.name for tool in include_tags_mcp.tools} == {\"get_item\"}\n\n    # Test exclude_tags\n    exclude_tags_mcp = FastApiMCP(app, exclude_tags=[\"write\", \"delete\"])\n    assert len(exclude_tags_mcp.tools) == 3\n    assert {tool.name for tool in exclude_tags_mcp.tools} == {\"get_item\", \"list_items\", \"search_items\"}\n\n    # Test combining include_operations and include_tags\n    combined_include_mcp = FastApiMCP(app, include_operations=[\"delete_item\"], include_tags=[\"search\"])\n    assert len(combined_include_mcp.tools) == 2\n    assert {tool.name for tool in combined_include_mcp.tools} == {\"delete_item\", \"search_items\"}\n\n    # Test invalid combinations\n    with pytest.raises(ValueError):\n        FastApiMCP(app, include_operations=[\"get_item\"], exclude_operations=[\"delete_item\"])\n\n    with pytest.raises(ValueError):\n        FastApiMCP(app, include_tags=[\"items\"], exclude_tags=[\"write\"])\n\n\ndef test_filtering_edge_cases():\n    \"\"\"Test edge cases for the filtering functionality.\"\"\"\n    app = FastAPI()\n\n    # Define endpoints with different operation IDs and tags\n    @app.get(\"/items/\", operation_id=\"list_items\", tags=[\"items\"])\n    async def list_items():\n        return [{\"id\": 1}]\n\n    @app.get(\"/items/{item_id}\", operation_id=\"get_item\", tags=[\"items\", \"read\"])\n    async def get_item(item_id: int):\n        return {\"id\": item_id}\n\n    # Test with no filtering (default behavior)\n    default_mcp = FastApiMCP(app)\n    assert len(default_mcp.tools) == 2\n    assert {tool.name for tool in default_mcp.tools} == {\"get_item\", \"list_items\"}\n\n    # Test with empty include_operations\n    empty_include_ops_mcp = FastApiMCP(app, include_operations=[])\n    assert len(empty_include_ops_mcp.tools) == 0\n    assert empty_include_ops_mcp.tools == []\n\n    # Test with empty exclude_operations (should include all)\n    empty_exclude_ops_mcp = FastApiMCP(app, exclude_operations=[])\n    assert len(empty_exclude_ops_mcp.tools) == 2\n    assert {tool.name for tool in empty_exclude_ops_mcp.tools} == {\"get_item\", \"list_items\"}\n\n    # Test with empty include_tags\n    empty_include_tags_mcp = FastApiMCP(app, include_tags=[])\n    assert len(empty_include_tags_mcp.tools) == 0\n    assert empty_include_tags_mcp.tools == []\n\n    # Test with empty exclude_tags (should include all)\n    empty_exclude_tags_mcp = FastApiMCP(app, exclude_tags=[])\n    assert len(empty_exclude_tags_mcp.tools) == 2\n    assert {tool.name for tool in empty_exclude_tags_mcp.tools} == {\"get_item\", \"list_items\"}\n\n    # Test with non-existent operation IDs\n    nonexistent_ops_mcp = FastApiMCP(app, include_operations=[\"non_existent_op\"])\n    assert len(nonexistent_ops_mcp.tools) == 0\n    assert nonexistent_ops_mcp.tools == []\n\n    # Test with non-existent tags\n    nonexistent_tags_mcp = FastApiMCP(app, include_tags=[\"non_existent_tag\"])\n    assert len(nonexistent_tags_mcp.tools) == 0\n    assert nonexistent_tags_mcp.tools == []\n\n    # Test excluding non-existent operation IDs\n    exclude_nonexistent_ops_mcp = FastApiMCP(app, exclude_operations=[\"non_existent_op\"])\n    assert len(exclude_nonexistent_ops_mcp.tools) == 2\n    assert {tool.name for tool in exclude_nonexistent_ops_mcp.tools} == {\"get_item\", \"list_items\"}\n\n    # Test excluding non-existent tags\n    exclude_nonexistent_tags_mcp = FastApiMCP(app, exclude_tags=[\"non_existent_tag\"])\n    assert len(exclude_nonexistent_tags_mcp.tools) == 2\n    assert {tool.name for tool in exclude_nonexistent_tags_mcp.tools} == {\"get_item\", \"list_items\"}\n\n    # Test with an endpoint that has no tags\n    @app.get(\"/no-tags\", operation_id=\"no_tags\")\n    async def no_tags():\n        return {\"result\": \"no tags\"}\n\n    # Test include_tags with an endpoint that has no tags\n    no_tags_app_mcp = FastApiMCP(app, include_tags=[\"items\"])\n    assert len(no_tags_app_mcp.tools) == 2\n    assert \"no_tags\" not in {tool.name for tool in no_tags_app_mcp.tools}\n\n    # Test exclude_tags with an endpoint that has no tags\n    no_tags_exclude_mcp = FastApiMCP(app, exclude_tags=[\"items\"])\n    assert len(no_tags_exclude_mcp.tools) == 1\n    assert {tool.name for tool in no_tags_exclude_mcp.tools} == {\"no_tags\"}\n\n\ndef test_filtering_with_missing_operation_ids():\n    \"\"\"Test filtering behavior with endpoints that don't have operation IDs.\"\"\"\n    app = FastAPI()\n\n    # Define an endpoint with an operation ID\n    @app.get(\"/items/\", operation_id=\"list_items\", tags=[\"items\"])\n    async def list_items():\n        return [{\"id\": 1}]\n\n    # Define an endpoint without an operation ID\n    @app.get(\"/no-op-id/\")\n    async def no_op_id():\n        return {\"result\": \"no operation ID\"}\n\n    # Test that both endpoints are discovered\n    default_mcp = FastApiMCP(app)\n\n    # FastAPI-MCP will generate an operation ID for endpoints without one\n    # The auto-generated ID will typically be 'no_op_id_no_op_id__get'\n    assert len(default_mcp.tools) == 2\n\n    # Get the auto-generated operation ID\n    auto_generated_op_id = None\n    for tool in default_mcp.tools:\n        if tool.name != \"list_items\":\n            auto_generated_op_id = tool.name\n            break\n\n    assert auto_generated_op_id is not None\n    assert \"list_items\" in {tool.name for tool in default_mcp.tools}\n\n    # Test include_operations with the known operation ID\n    include_ops_mcp = FastApiMCP(app, include_operations=[\"list_items\"])\n    assert len(include_ops_mcp.tools) == 1\n    assert {tool.name for tool in include_ops_mcp.tools} == {\"list_items\"}\n\n    # Test include_operations with the auto-generated operation ID\n    include_auto_ops_mcp = FastApiMCP(app, include_operations=[auto_generated_op_id])\n    assert len(include_auto_ops_mcp.tools) == 1\n    assert {tool.name for tool in include_auto_ops_mcp.tools} == {auto_generated_op_id}\n\n    # Test include_tags with a tag that matches the endpoint\n    include_tags_mcp = FastApiMCP(app, include_tags=[\"items\"])\n    assert len(include_tags_mcp.tools) == 1\n    assert {tool.name for tool in include_tags_mcp.tools} == {\"list_items\"}\n\n\ndef test_filter_with_empty_tools():\n    \"\"\"Test filtering with an empty tools list to ensure it handles this edge case correctly.\"\"\"\n    # Create a FastAPI app without any routes\n    app = FastAPI()\n\n    # Create MCP server (should have no tools)\n    empty_mcp = FastApiMCP(app)\n    assert len(empty_mcp.tools) == 0\n\n    # Test filtering with various options on an empty app\n    include_ops_mcp = FastApiMCP(app, include_operations=[\"some_op\"])\n    assert len(include_ops_mcp.tools) == 0\n\n    exclude_ops_mcp = FastApiMCP(app, exclude_operations=[\"some_op\"])\n    assert len(exclude_ops_mcp.tools) == 0\n\n    include_tags_mcp = FastApiMCP(app, include_tags=[\"some_tag\"])\n    assert len(include_tags_mcp.tools) == 0\n\n    exclude_tags_mcp = FastApiMCP(app, exclude_tags=[\"some_tag\"])\n    assert len(exclude_tags_mcp.tools) == 0\n\n    # Test combined filtering\n    combined_mcp = FastApiMCP(app, include_operations=[\"op\"], include_tags=[\"tag\"])\n    assert len(combined_mcp.tools) == 0\n\n\ndef test_filtering_with_empty_tags_array():\n    \"\"\"Test filtering behavior with endpoints that have empty tags array.\"\"\"\n    app = FastAPI()\n\n    # Define an endpoint with tags\n    @app.get(\"/items/\", operation_id=\"list_items\", tags=[\"items\"])\n    async def list_items():\n        return [{\"id\": 1}]\n\n    # Define an endpoint with an empty tags array\n    @app.get(\"/empty-tags/\", operation_id=\"empty_tags\", tags=[])\n    async def empty_tags():\n        return {\"result\": \"empty tags\"}\n\n    # Test default behavior\n    default_mcp = FastApiMCP(app)\n    assert len(default_mcp.tools) == 2\n    assert {tool.name for tool in default_mcp.tools} == {\"list_items\", \"empty_tags\"}\n\n    # Test include_tags\n    include_tags_mcp = FastApiMCP(app, include_tags=[\"items\"])\n    assert len(include_tags_mcp.tools) == 1\n    assert {tool.name for tool in include_tags_mcp.tools} == {\"list_items\"}\n\n    # Test exclude_tags\n    exclude_tags_mcp = FastApiMCP(app, exclude_tags=[\"items\"])\n    assert len(exclude_tags_mcp.tools) == 1\n    assert {tool.name for tool in exclude_tags_mcp.tools} == {\"empty_tags\"}\n"
  },
  {
    "path": "tests/test_http_real_transport.py",
    "content": "import multiprocessing\nimport socket\nimport time\nimport os\nimport signal\nimport atexit\nimport sys\nimport threading\nimport coverage\nfrom typing import AsyncGenerator, Generator\nfrom fastapi import FastAPI\nimport pytest\nimport httpx\nimport uvicorn\nfrom fastapi_mcp import FastApiMCP\nimport mcp.types as types\n\n\nHOST = \"127.0.0.1\"\nSERVER_NAME = \"Test MCP Server\"\n\n\ndef run_server(server_port: int, fastapi_app: FastAPI) -> None:\n    # Initialize coverage for subprocesses\n    cov = None\n    if \"COVERAGE_PROCESS_START\" in os.environ:\n        cov = coverage.Coverage(source=[\"fastapi_mcp\"])\n        cov.start()\n\n        # Create a function to save coverage data at exit\n        def cleanup():\n            if cov:\n                cov.stop()\n                cov.save()\n\n        # Register multiple cleanup mechanisms to ensure coverage data is saved\n        atexit.register(cleanup)\n\n        # Setup signal handler for clean termination\n        def handle_signal(signum, frame):\n            cleanup()\n            sys.exit(0)\n\n        signal.signal(signal.SIGTERM, handle_signal)\n\n        # Backup thread to ensure coverage is written if process is terminated abruptly\n        def periodic_save():\n            while True:\n                time.sleep(1.0)\n                if cov:\n                    cov.save()\n\n        save_thread = threading.Thread(target=periodic_save)\n        save_thread.daemon = True\n        save_thread.start()\n\n    # Configure the server\n    mcp = FastApiMCP(\n        fastapi_app,\n        name=SERVER_NAME,\n        description=\"Test description\",\n    )\n    mcp.mount_http()\n\n    # Start the server\n    server = uvicorn.Server(config=uvicorn.Config(app=fastapi_app, host=HOST, port=server_port, log_level=\"error\"))\n    server.run()\n\n    # Give server time to start\n    while not server.started:\n        time.sleep(0.5)\n\n    # Ensure coverage is saved if exiting the normal way\n    if cov:\n        cov.stop()\n        cov.save()\n\n\n@pytest.fixture(params=[\"simple_fastapi_app\", \"simple_fastapi_app_with_root_path\"])\ndef server(request: pytest.FixtureRequest) -> Generator[str, None, None]:\n    # Ensure COVERAGE_PROCESS_START is set in the environment for subprocesses\n    coverage_rc = os.path.abspath(\".coveragerc\")\n    os.environ[\"COVERAGE_PROCESS_START\"] = coverage_rc\n\n    # Get a free port\n    with socket.socket() as s:\n        s.bind((HOST, 0))\n        server_port = s.getsockname()[1]\n\n    # Use fork method to avoid pickling issues\n    ctx = multiprocessing.get_context(\"fork\")\n\n    # Run the server in a subprocess\n    fastapi_app = request.getfixturevalue(request.param)\n    proc = ctx.Process(\n        target=run_server,\n        kwargs={\"server_port\": server_port, \"fastapi_app\": fastapi_app},\n        daemon=True,\n    )\n    proc.start()\n\n    # Wait for server to be running\n    max_attempts = 20\n    attempt = 0\n    while attempt < max_attempts:\n        try:\n            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:\n                s.connect((HOST, server_port))\n                break\n        except ConnectionRefusedError:\n            time.sleep(0.1)\n            attempt += 1\n    else:\n        raise RuntimeError(f\"Server failed to start after {max_attempts} attempts\")\n\n    # Return the server URL\n    yield f\"http://{HOST}:{server_port}{fastapi_app.root_path}\"\n\n    # Signal the server to stop - added graceful shutdown before kill\n    try:\n        proc.terminate()\n        proc.join(timeout=2)\n    except (OSError, AttributeError):\n        pass\n\n    if proc.is_alive():\n        proc.kill()\n        proc.join(timeout=2)\n        if proc.is_alive():\n            raise RuntimeError(\"server process failed to terminate\")\n\n\n@pytest.fixture()\nasync def http_client(server: str) -> AsyncGenerator[httpx.AsyncClient, None]:\n    async with httpx.AsyncClient(base_url=server) as client:\n        yield client\n\n\n@pytest.mark.anyio\nasync def test_http_initialize_request(http_client: httpx.AsyncClient, server: str) -> None:\n    mcp_path = \"/mcp\"  # Always use absolute path since server already includes root_path\n\n    response = await http_client.post(\n        mcp_path,\n        json={\n            \"jsonrpc\": \"2.0\",\n            \"method\": \"initialize\",\n            \"id\": 1,\n            \"params\": {\n                \"protocolVersion\": types.LATEST_PROTOCOL_VERSION,\n                \"capabilities\": {\n                    \"sampling\": None,\n                    \"elicitation\": None,\n                    \"experimental\": None,\n                    \"roots\": None,\n                },\n                \"clientInfo\": {\"name\": \"test-client\", \"version\": \"1.0.0\"},\n            },\n        },\n        headers={\"Accept\": \"application/json, text/event-stream\", \"Content-Type\": \"application/json\"},\n    )\n\n    assert response.status_code == 200\n\n    result = response.json()\n    assert result[\"jsonrpc\"] == \"2.0\"\n    assert result[\"id\"] == 1\n    assert \"result\" in result\n    assert result[\"result\"][\"serverInfo\"][\"name\"] == SERVER_NAME\n\n\n@pytest.mark.anyio\nasync def test_http_list_tools(http_client: httpx.AsyncClient, server: str) -> None:\n    \"\"\"Test tool listing via HTTP POST with JSON response.\"\"\"\n    mcp_path = \"/mcp\"\n\n    init_response = await http_client.post(\n        mcp_path,\n        json={\n            \"jsonrpc\": \"2.0\",\n            \"method\": \"initialize\",\n            \"id\": 1,\n            \"params\": {\n                \"protocolVersion\": types.LATEST_PROTOCOL_VERSION,\n                \"capabilities\": {},\n                \"clientInfo\": {\"name\": \"test-client\", \"version\": \"1.0.0\"},\n            },\n        },\n        headers={\"Accept\": \"application/json, text/event-stream\", \"Content-Type\": \"application/json\"},\n    )\n    assert init_response.status_code == 200\n\n    # Extract session ID from the initialize response\n    session_id = init_response.headers.get(\"mcp-session-id\")\n    assert session_id is not None, \"Server should return a session ID\"\n\n    initialized_response = await http_client.post(\n        mcp_path,\n        json={\n            \"jsonrpc\": \"2.0\",\n            \"method\": \"notifications/initialized\",\n        },\n        headers={\n            \"Accept\": \"application/json, text/event-stream\",\n            \"Content-Type\": \"application/json\",\n            \"mcp-session-id\": session_id,\n        },\n    )\n    assert initialized_response.status_code == 202\n\n    response = await http_client.post(\n        mcp_path,\n        json={\n            \"jsonrpc\": \"2.0\",\n            \"method\": \"tools/list\",\n            \"id\": 2,\n        },\n        headers={\n            \"Accept\": \"application/json, text/event-stream\",\n            \"Content-Type\": \"application/json\",\n            \"mcp-session-id\": session_id,\n        },\n    )\n\n    assert response.status_code == 200\n    result = response.json()\n    assert result[\"jsonrpc\"] == \"2.0\"\n    assert result[\"id\"] == 2\n    assert \"result\" in result\n    assert \"tools\" in result[\"result\"]\n    assert len(result[\"result\"][\"tools\"]) > 0\n\n    # Verify we have the expected tools from the simple FastAPI app\n    tool_names = [tool[\"name\"] for tool in result[\"result\"][\"tools\"]]\n    assert \"get_item\" in tool_names\n    assert \"list_items\" in tool_names\n\n\n@pytest.mark.anyio\nasync def test_http_call_tool(http_client: httpx.AsyncClient, server: str) -> None:\n    \"\"\"Test tool calling via HTTP POST with JSON response.\"\"\"\n    mcp_path = \"/mcp\"\n\n    init_response = await http_client.post(\n        mcp_path,\n        json={\n            \"jsonrpc\": \"2.0\",\n            \"method\": \"initialize\",\n            \"id\": 1,\n            \"params\": {\n                \"protocolVersion\": types.LATEST_PROTOCOL_VERSION,\n                \"capabilities\": {},\n                \"clientInfo\": {\"name\": \"test-client\", \"version\": \"1.0.0\"},\n            },\n        },\n        headers={\"Accept\": \"application/json, text/event-stream\", \"Content-Type\": \"application/json\"},\n    )\n    assert init_response.status_code == 200\n\n    # Extract session ID from the initialize response\n    session_id = init_response.headers.get(\"mcp-session-id\")\n    assert session_id is not None, \"Server should return a session ID\"\n\n    initialized_response = await http_client.post(\n        mcp_path,\n        json={\n            \"jsonrpc\": \"2.0\",\n            \"method\": \"notifications/initialized\",\n        },\n        headers={\n            \"Accept\": \"application/json, text/event-stream\",\n            \"Content-Type\": \"application/json\",\n            \"mcp-session-id\": session_id,\n        },\n    )\n    assert initialized_response.status_code == 202\n\n    response = await http_client.post(\n        mcp_path,\n        json={\n            \"jsonrpc\": \"2.0\",\n            \"method\": \"tools/call\",\n            \"id\": 3,\n            \"params\": {\n                \"name\": \"get_item\",\n                \"arguments\": {\"item_id\": 1},\n            },\n        },\n        headers={\n            \"Accept\": \"application/json, text/event-stream\",\n            \"Content-Type\": \"application/json\",\n            \"mcp-session-id\": session_id,\n        },\n    )\n\n    assert response.status_code == 200\n    result = response.json()\n    assert result[\"jsonrpc\"] == \"2.0\"\n    assert result[\"id\"] == 3\n    assert \"result\" in result\n    assert result[\"result\"][\"isError\"] is False\n    assert \"content\" in result[\"result\"]\n    assert len(result[\"result\"][\"content\"]) > 0\n\n    # Verify the response contains expected item data\n    content = result[\"result\"][\"content\"][0]\n    assert content[\"type\"] == \"text\"\n    assert \"Item 1\" in content[\"text\"]  # Should contain the item name\n\n\n@pytest.mark.anyio\nasync def test_http_ping(http_client: httpx.AsyncClient, server: str) -> None:\n    \"\"\"Test ping functionality via HTTP POST.\"\"\"\n    mcp_path = \"/mcp\"\n\n    init_response = await http_client.post(\n        mcp_path,\n        json={\n            \"jsonrpc\": \"2.0\",\n            \"method\": \"initialize\",\n            \"id\": 1,\n            \"params\": {\n                \"protocolVersion\": types.LATEST_PROTOCOL_VERSION,\n                \"capabilities\": {},\n                \"clientInfo\": {\"name\": \"test-client\", \"version\": \"1.0.0\"},\n            },\n        },\n        headers={\"Accept\": \"application/json, text/event-stream\", \"Content-Type\": \"application/json\"},\n    )\n    assert init_response.status_code == 200\n\n    # Extract session ID from the initialize response\n    session_id = init_response.headers.get(\"mcp-session-id\")\n    assert session_id is not None, \"Server should return a session ID\"\n\n    initialized_response = await http_client.post(\n        mcp_path,\n        json={\n            \"jsonrpc\": \"2.0\",\n            \"method\": \"notifications/initialized\",\n        },\n        headers={\n            \"Accept\": \"application/json, text/event-stream\",\n            \"Content-Type\": \"application/json\",\n            \"mcp-session-id\": session_id,\n        },\n    )\n    assert initialized_response.status_code == 202\n\n    response = await http_client.post(\n        mcp_path,\n        json={\n            \"jsonrpc\": \"2.0\",\n            \"method\": \"ping\",\n            \"id\": 4,\n        },\n        headers={\n            \"Accept\": \"application/json, text/event-stream\",\n            \"Content-Type\": \"application/json\",\n            \"mcp-session-id\": session_id,\n        },\n    )\n\n    assert response.status_code == 200\n    result = response.json()\n    assert result[\"jsonrpc\"] == \"2.0\"\n    assert result[\"id\"] == 4\n    assert \"result\" in result\n\n\n@pytest.mark.anyio\nasync def test_http_error_handling(http_client: httpx.AsyncClient, server: str) -> None:\n    \"\"\"Test error handling for invalid requests.\"\"\"\n    mcp_path = \"/mcp\"\n\n    response = await http_client.post(\n        mcp_path,\n        content=\"invalid json\",\n        headers={\"Accept\": \"application/json, text/event-stream\", \"Content-Type\": \"application/json\"},\n    )\n\n    assert response.status_code == 400\n    result = response.json()\n    assert \"error\" in result\n    assert result[\"error\"][\"code\"] == -32700  # Parse error\n\n\n@pytest.mark.anyio\nasync def test_http_invalid_method(http_client: httpx.AsyncClient, server: str) -> None:\n    \"\"\"Test error handling for invalid methods.\"\"\"\n    mcp_path = \"/mcp\"\n\n    # First initialize to get a session ID\n    init_response = await http_client.post(\n        mcp_path,\n        json={\n            \"jsonrpc\": \"2.0\",\n            \"method\": \"initialize\",\n            \"id\": 1,\n            \"params\": {\n                \"protocolVersion\": types.LATEST_PROTOCOL_VERSION,\n                \"capabilities\": {},\n                \"clientInfo\": {\"name\": \"test-client\", \"version\": \"1.0.0\"},\n            },\n        },\n        headers={\"Accept\": \"application/json, text/event-stream\", \"Content-Type\": \"application/json\"},\n    )\n    assert init_response.status_code == 200\n\n    # Extract session ID from the initialize response\n    session_id = init_response.headers.get(\"mcp-session-id\")\n    assert session_id is not None, \"Server should return a session ID\"\n\n    response = await http_client.post(\n        mcp_path,\n        json={\n            \"jsonrpc\": \"2.0\",\n            \"method\": \"invalid/method\",\n            \"id\": 5,\n        },\n        headers={\n            \"Accept\": \"application/json, text/event-stream\",\n            \"Content-Type\": \"application/json\",\n            \"mcp-session-id\": session_id,\n        },\n    )\n\n    assert response.status_code == 200\n    result = response.json()\n    assert result[\"jsonrpc\"] == \"2.0\"\n    assert result[\"id\"] == 5\n    assert \"error\" in result\n    assert result[\"error\"][\"code\"] == -32602  # Invalid request parameters\n\n\n@pytest.mark.anyio\nasync def test_http_notification_handling(http_client: httpx.AsyncClient, server: str) -> None:\n    \"\"\"Test that notifications return 202 Accepted without response body.\"\"\"\n    mcp_path = \"/mcp\"\n\n    # First initialize to get a session ID\n    init_response = await http_client.post(\n        mcp_path,\n        json={\n            \"jsonrpc\": \"2.0\",\n            \"method\": \"initialize\",\n            \"id\": 1,\n            \"params\": {\n                \"protocolVersion\": types.LATEST_PROTOCOL_VERSION,\n                \"capabilities\": {},\n                \"clientInfo\": {\"name\": \"test-client\", \"version\": \"1.0.0\"},\n            },\n        },\n        headers={\"Accept\": \"application/json, text/event-stream\", \"Content-Type\": \"application/json\"},\n    )\n    assert init_response.status_code == 200\n\n    # Extract session ID from the initialize response\n    session_id = init_response.headers.get(\"mcp-session-id\")\n    assert session_id is not None, \"Server should return a session ID\"\n\n    response = await http_client.post(\n        mcp_path,\n        json={\n            \"jsonrpc\": \"2.0\",\n            \"method\": \"notifications/cancelled\",\n            \"params\": {\"requestId\": \"test-123\"},\n        },\n        headers={\n            \"Accept\": \"application/json, text/event-stream\",\n            \"Content-Type\": \"application/json\",\n            \"mcp-session-id\": session_id,\n        },\n    )\n\n    assert response.status_code == 202\n    # Notifications should return empty body\n    assert response.content == b\"\" or response.text == \"null\"\n"
  },
  {
    "path": "tests/test_mcp_complex_app.py",
    "content": "import json\n\nimport pytest\nimport mcp.types as types\nfrom mcp.server.lowlevel import Server\nfrom mcp.shared.memory import create_connected_server_and_client_session\nfrom fastapi import FastAPI\n\nfrom fastapi_mcp import FastApiMCP\n\nfrom .fixtures.types import Product, Customer, OrderResponse\n\n\n@pytest.fixture\ndef fastapi_mcp(complex_fastapi_app: FastAPI) -> FastApiMCP:\n    mcp = FastApiMCP(\n        complex_fastapi_app,\n        name=\"Test MCP Server\",\n        description=\"Test description\",\n    )\n    mcp.mount()\n    return mcp\n\n\n@pytest.fixture\ndef lowlevel_server_complex_app(fastapi_mcp: FastApiMCP) -> Server:\n    return fastapi_mcp.server\n\n\n@pytest.mark.asyncio\nasync def test_list_tools(lowlevel_server_complex_app: Server):\n    async with create_connected_server_and_client_session(lowlevel_server_complex_app) as client_session:\n        tools_result = await client_session.list_tools()\n\n        assert len(tools_result.tools) > 0\n\n        tool_names = [tool.name for tool in tools_result.tools]\n        expected_operations = [\"list_products\", \"get_product\", \"create_order\", \"get_customer\"]\n        for op in expected_operations:\n            assert op in tool_names\n\n\n@pytest.mark.asyncio\nasync def test_call_tool_list_products_default(lowlevel_server_complex_app: Server):\n    async with create_connected_server_and_client_session(lowlevel_server_complex_app) as client_session:\n        response = await client_session.call_tool(\"list_products\", {})\n\n        assert not response.isError\n        assert len(response.content) > 0\n\n        text_content = next(c for c in response.content if isinstance(c, types.TextContent))\n        result = json.loads(text_content.text)\n\n        assert \"items\" in result\n        assert result[\"total\"] == 1\n        assert result[\"page\"] == 1\n        assert len(result[\"items\"]) == 1\n\n\n@pytest.mark.asyncio\nasync def test_call_tool_list_products_with_filters(lowlevel_server_complex_app: Server):\n    async with create_connected_server_and_client_session(lowlevel_server_complex_app) as client_session:\n        response = await client_session.call_tool(\n            \"list_products\",\n            {\"category\": \"electronics\", \"min_price\": 10.0, \"page\": 1, \"size\": 10, \"in_stock_only\": True},\n        )\n\n        assert not response.isError\n        assert len(response.content) > 0\n\n        text_content = next(c for c in response.content if isinstance(c, types.TextContent))\n        result = json.loads(text_content.text)\n\n        assert \"items\" in result\n        assert result[\"page\"] == 1\n        assert result[\"size\"] == 10\n\n\n@pytest.mark.asyncio\nasync def test_call_tool_get_product(lowlevel_server_complex_app: Server, example_product: Product):\n    product_id = \"123e4567-e89b-12d3-a456-426614174000\"  # Valid UUID format\n\n    async with create_connected_server_and_client_session(lowlevel_server_complex_app) as client_session:\n        response = await client_session.call_tool(\"get_product\", {\"product_id\": product_id})\n\n        assert not response.isError\n        assert len(response.content) > 0\n\n        text_content = next(c for c in response.content if isinstance(c, types.TextContent))\n        result = json.loads(text_content.text)\n\n        assert result[\"id\"] == product_id\n        assert \"name\" in result\n        assert \"price\" in result\n        assert \"description\" in result\n\n\n@pytest.mark.asyncio\nasync def test_call_tool_get_product_with_options(lowlevel_server_complex_app: Server):\n    product_id = \"123e4567-e89b-12d3-a456-426614174000\"  # Valid UUID format\n\n    async with create_connected_server_and_client_session(lowlevel_server_complex_app) as client_session:\n        response = await client_session.call_tool(\n            \"get_product\", {\"product_id\": product_id, \"include_unavailable\": True}\n        )\n\n        assert not response.isError\n        assert len(response.content) > 0\n\n        text_content = next(c for c in response.content if isinstance(c, types.TextContent))\n        result = json.loads(text_content.text)\n\n        assert result[\"id\"] == product_id\n\n\n@pytest.mark.asyncio\nasync def test_call_tool_create_order(lowlevel_server_complex_app: Server, example_order_response: OrderResponse):\n    customer_id = \"123e4567-e89b-12d3-a456-426614174000\"  # Valid UUID format\n    product_id = \"123e4567-e89b-12d3-a456-426614174001\"  # Valid UUID format\n    shipping_address_id = \"123e4567-e89b-12d3-a456-426614174002\"  # Valid UUID format\n\n    order_request = {\n        \"customer_id\": customer_id,\n        \"items\": [{\"product_id\": product_id, \"quantity\": 2, \"unit_price\": 29.99, \"total\": 59.98}],\n        \"shipping_address_id\": shipping_address_id,\n        \"payment_method\": \"credit_card\",\n    }\n\n    async with create_connected_server_and_client_session(lowlevel_server_complex_app) as client_session:\n        response = await client_session.call_tool(\"create_order\", order_request)\n\n        assert not response.isError\n        assert len(response.content) > 0\n\n        text_content = next(c for c in response.content if isinstance(c, types.TextContent))\n        result = json.loads(text_content.text)\n\n        assert result[\"customer_id\"] == customer_id\n        assert \"id\" in result\n        assert \"status\" in result\n        assert \"items\" in result\n        assert len(result[\"items\"]) > 0\n\n\n@pytest.mark.asyncio\nasync def test_call_tool_create_order_validation_error(lowlevel_server_complex_app: Server):\n    # Missing required fields\n    order_request = {\n        # Missing customer_id\n        \"items\": [],\n        # Missing shipping_address_id\n        \"payment_method\": \"credit_card\",\n    }\n\n    async with create_connected_server_and_client_session(lowlevel_server_complex_app) as client_session:\n        response = await client_session.call_tool(\"create_order\", order_request)\n\n        assert response.isError\n        assert len(response.content) > 0\n\n        text_content = next(c for c in response.content if isinstance(c, types.TextContent))\n        assert \"422\" in text_content.text or \"validation\" in text_content.text.lower()\n\n\n@pytest.mark.asyncio\nasync def test_call_tool_get_customer(lowlevel_server_complex_app: Server, example_customer: Customer):\n    customer_id = \"123e4567-e89b-12d3-a456-426614174000\"  # Valid UUID format\n\n    async with create_connected_server_and_client_session(lowlevel_server_complex_app) as client_session:\n        response = await client_session.call_tool(\"get_customer\", {\"customer_id\": customer_id})\n\n        assert not response.isError\n        assert len(response.content) > 0\n\n        text_content = next(c for c in response.content if isinstance(c, types.TextContent))\n        result = json.loads(text_content.text)\n\n        assert result[\"id\"] == customer_id\n        assert \"full_name\" in result\n        assert \"email\" in result\n\n\n@pytest.mark.asyncio\nasync def test_call_tool_get_customer_with_options(lowlevel_server_complex_app: Server):\n    customer_id = \"123e4567-e89b-12d3-a456-426614174000\"  # Valid UUID format\n\n    async with create_connected_server_and_client_session(lowlevel_server_complex_app) as client_session:\n        response = await client_session.call_tool(\n            \"get_customer\",\n            {\n                \"customer_id\": customer_id,\n                \"include_orders\": True,\n                \"include_payment_methods\": True,\n                \"fields\": [\"full_name\", \"email\", \"orders\"],\n            },\n        )\n\n        assert not response.isError\n        assert len(response.content) > 0\n\n        text_content = next(c for c in response.content if isinstance(c, types.TextContent))\n        result = json.loads(text_content.text)\n\n        assert result[\"id\"] == customer_id\n\n\n@pytest.mark.asyncio\nasync def test_error_handling_missing_parameter(lowlevel_server_complex_app: Server):\n    async with create_connected_server_and_client_session(lowlevel_server_complex_app) as client_session:\n        # Missing required product_id parameter\n        response = await client_session.call_tool(\"get_product\", {})\n\n        assert response.isError\n        assert len(response.content) > 0\n\n        text_content = next(c for c in response.content if isinstance(c, types.TextContent))\n        assert \"input validation error\" in text_content.text.lower(), \"Expected an input validation error\"\n        assert \"required\" in text_content.text.lower(), \"Expected a missing required parameter error\"\n"
  },
  {
    "path": "tests/test_mcp_execute_api_tool.py",
    "content": "import pytest\nfrom unittest.mock import AsyncMock, patch, MagicMock\nfrom fastapi import FastAPI\n\nfrom fastapi_mcp import FastApiMCP\nfrom mcp.types import TextContent\n\n\n@pytest.mark.asyncio\nasync def test_execute_api_tool_success(simple_fastapi_app: FastAPI):\n    \"\"\"Test successful execution of an API tool.\"\"\"\n    mcp = FastApiMCP(simple_fastapi_app)\n    \n    # Mock the HTTP client response\n    mock_response = MagicMock()\n    mock_response.json.return_value = {\"id\": 1, \"name\": \"Test Item\"}\n    mock_response.status_code = 200\n    mock_response.text = '{\"id\": 1, \"name\": \"Test Item\"}'\n    \n    # Mock the HTTP client\n    mock_client = AsyncMock()\n    mock_client.get.return_value = mock_response\n    \n    # Test parameters\n    tool_name = \"get_item\"\n    arguments = {\"item_id\": 1}\n    \n    # Execute the tool\n    with patch.object(mcp, '_http_client', mock_client):\n        result = await mcp._execute_api_tool(\n            client=mock_client,\n            tool_name=tool_name,\n            arguments=arguments,\n            operation_map=mcp.operation_map\n        )\n    \n    # Verify the result\n    assert len(result) == 1\n    assert isinstance(result[0], TextContent)\n    assert result[0].text == '{\\n  \"id\": 1,\\n  \"name\": \"Test Item\"\\n}'\n    \n    # Verify the HTTP client was called correctly\n    mock_client.get.assert_called_once_with(\n        \"/items/1\",\n        params={},\n        headers={}\n    )\n\n\n@pytest.mark.asyncio\nasync def test_execute_api_tool_with_query_params(simple_fastapi_app: FastAPI):\n    \"\"\"Test execution of an API tool with query parameters.\"\"\"\n    mcp = FastApiMCP(simple_fastapi_app)\n    \n    # Mock the HTTP client response\n    mock_response = MagicMock()\n    mock_response.json.return_value = [{\"id\": 1, \"name\": \"Item 1\"}, {\"id\": 2, \"name\": \"Item 2\"}]\n    mock_response.status_code = 200\n    mock_response.text = '[{\"id\": 1, \"name\": \"Item 1\"}, {\"id\": 2, \"name\": \"Item 2\"}]'\n    \n    # Mock the HTTP client\n    mock_client = AsyncMock()\n    mock_client.get.return_value = mock_response\n    \n    # Test parameters\n    tool_name = \"list_items\"\n    arguments = {\"skip\": 0, \"limit\": 2}\n    \n    # Execute the tool\n    with patch.object(mcp, '_http_client', mock_client):\n        result = await mcp._execute_api_tool(\n            client=mock_client,\n            tool_name=tool_name,\n            arguments=arguments,\n            operation_map=mcp.operation_map\n        )\n    \n    # Verify the result\n    assert len(result) == 1\n    assert isinstance(result[0], TextContent)\n    \n    # Verify the HTTP client was called with query parameters\n    mock_client.get.assert_called_once_with(\n        \"/items/\",\n        params={\"skip\": 0, \"limit\": 2},\n        headers={}\n    )\n\n\n@pytest.mark.asyncio\nasync def test_execute_api_tool_with_body(simple_fastapi_app: FastAPI):\n    \"\"\"Test execution of an API tool with request body.\"\"\"\n    mcp = FastApiMCP(simple_fastapi_app)\n    \n    # Mock the HTTP client response\n    mock_response = MagicMock()\n    mock_response.json.return_value = {\"id\": 1, \"name\": \"New Item\"}\n    mock_response.status_code = 200\n    mock_response.text = '{\"id\": 1, \"name\": \"New Item\"}'\n    \n    # Mock the HTTP client\n    mock_client = AsyncMock()\n    mock_client.post.return_value = mock_response\n    \n    # Test parameters\n    tool_name = \"create_item\"\n    arguments = {\n        \"item\": {\n            \"id\": 1,\n            \"name\": \"New Item\",\n            \"price\": 10.0,\n            \"tags\": [\"tag1\"],\n            \"description\": \"New item description\"\n        }\n    }\n    \n    # Execute the tool\n    with patch.object(mcp, '_http_client', mock_client):\n        result = await mcp._execute_api_tool(\n            client=mock_client,\n            tool_name=tool_name,\n            arguments=arguments,\n            operation_map=mcp.operation_map\n        )\n    \n    # Verify the result\n    assert len(result) == 1\n    assert isinstance(result[0], TextContent)\n    \n    # Verify the HTTP client was called with the request body\n    mock_client.post.assert_called_once_with(\n        \"/items/\",\n        params={},\n        headers={},\n        json=arguments\n    )\n\n\n@pytest.mark.asyncio\nasync def test_execute_api_tool_with_non_ascii_chars(simple_fastapi_app: FastAPI):\n    \"\"\"Test execution of an API tool with non-ASCII characters.\"\"\"\n    mcp = FastApiMCP(simple_fastapi_app)\n    \n    # Test data with both ASCII and non-ASCII characters\n    test_data = {\n        \"id\": 1,\n        \"name\": \"你好 World\",  # Chinese characters + ASCII\n        \"price\": 10.0,\n        \"tags\": [\"tag1\", \"标签2\"],  # Chinese characters in tags\n        \"description\": \"这是一个测试描述\"  # All Chinese characters\n    }\n    \n    # Mock the HTTP client response\n    mock_response = MagicMock()\n    mock_response.json.return_value = test_data\n    mock_response.status_code = 200\n    mock_response.text = '{\"id\": 1, \"name\": \"你好 World\", \"price\": 10.0, \"tags\": [\"tag1\", \"标签2\"], \"description\": \"这是一个测试描述\"}'\n    \n    # Mock the HTTP client\n    mock_client = AsyncMock()\n    mock_client.get.return_value = mock_response\n    \n    # Test parameters\n    tool_name = \"get_item\"\n    arguments = {\"item_id\": 1}\n    \n    # Execute the tool\n    with patch.object(mcp, '_http_client', mock_client):\n        result = await mcp._execute_api_tool(\n            client=mock_client,\n            tool_name=tool_name,\n            arguments=arguments,\n            operation_map=mcp.operation_map\n        )\n    \n    # Verify the result\n    assert len(result) == 1\n    assert isinstance(result[0], TextContent)\n    \n    # Verify that the response contains both ASCII and non-ASCII characters\n    response_text = result[0].text\n    assert \"你好\" in response_text  # Chinese characters preserved\n    assert \"World\" in response_text  # ASCII characters preserved\n    assert \"标签2\" in response_text  # Chinese characters in tags preserved\n    assert \"这是一个测试描述\" in response_text  # All Chinese description preserved\n    \n    # Verify the HTTP client was called correctly\n    mock_client.get.assert_called_once_with(\n        \"/items/1\",\n        params={},\n        headers={}\n    )\n"
  },
  {
    "path": "tests/test_mcp_simple_app.py",
    "content": "import json\n\nimport pytest\nimport mcp.types as types\nfrom mcp.server.lowlevel import Server\nfrom mcp.shared.memory import create_connected_server_and_client_session\nfrom fastapi import FastAPI\n\nfrom fastapi_mcp import FastApiMCP\n\nfrom .fixtures.types import Item\n\n\n@pytest.fixture\ndef fastapi_mcp(simple_fastapi_app: FastAPI) -> FastApiMCP:\n    mcp = FastApiMCP(\n        simple_fastapi_app,\n        name=\"Test MCP Server\",\n        description=\"Test description\",\n    )\n    mcp.mount()\n    return mcp\n\n\n@pytest.fixture\ndef fastapi_mcp_with_custom_header(simple_fastapi_app: FastAPI) -> FastApiMCP:\n    mcp = FastApiMCP(\n        simple_fastapi_app,\n        name=\"Test MCP Server with custom header\",\n        description=\"Test description\",\n        headers=[\"X-Custom-Header\"],\n    )\n    mcp.mount()\n    return mcp\n\n\n@pytest.fixture\ndef lowlevel_server_simple_app(fastapi_mcp: FastApiMCP) -> Server:\n    return fastapi_mcp.server\n\n\n@pytest.mark.asyncio\nasync def test_list_tools(lowlevel_server_simple_app: Server):\n    \"\"\"Test listing tools via direct MCP connection.\"\"\"\n    async with create_connected_server_and_client_session(lowlevel_server_simple_app) as client_session:\n        tools_result = await client_session.list_tools()\n\n        assert len(tools_result.tools) > 0\n\n        tool_names = [tool.name for tool in tools_result.tools]\n        expected_operations = [\"list_items\", \"get_item\", \"create_item\", \"update_item\", \"delete_item\", \"raise_error\"]\n        for op in expected_operations:\n            assert op in tool_names\n\n\n@pytest.mark.asyncio\nasync def test_call_tool_get_item_1(lowlevel_server_simple_app: Server):\n    async with create_connected_server_and_client_session(lowlevel_server_simple_app) as client_session:\n        response = await client_session.call_tool(\"get_item\", {\"item_id\": 1})\n\n        assert not response.isError\n        assert len(response.content) > 0\n\n        text_content = next(c for c in response.content if isinstance(c, types.TextContent))\n        result: dict = json.loads(text_content.text)\n        parsed_result = Item(**result)\n\n        assert parsed_result.id == 1\n        assert parsed_result.name == \"Item 1\"\n        assert parsed_result.price == 10.0\n        assert parsed_result.tags == [\"tag1\", \"tag2\"]\n\n\n@pytest.mark.asyncio\nasync def test_call_tool_get_item_2(lowlevel_server_simple_app: Server):\n    async with create_connected_server_and_client_session(lowlevel_server_simple_app) as client_session:\n        response = await client_session.call_tool(\"get_item\", {\"item_id\": 2})\n\n        assert not response.isError\n        assert len(response.content) > 0\n\n        text_content = next(c for c in response.content if isinstance(c, types.TextContent))\n        result: dict = json.loads(text_content.text)\n        parsed_result = Item(**result)\n\n        assert parsed_result.id == 2\n        assert parsed_result.name == \"Item 2\"\n        assert parsed_result.price == 20.0\n        assert parsed_result.tags == [\"tag2\", \"tag3\"]\n\n\n@pytest.mark.asyncio\nasync def test_call_tool_raise_error(lowlevel_server_simple_app: Server):\n    async with create_connected_server_and_client_session(lowlevel_server_simple_app) as client_session:\n        response = await client_session.call_tool(\"raise_error\", {})\n\n        assert response.isError\n        assert len(response.content) > 0\n\n        text_content = next(c for c in response.content if isinstance(c, types.TextContent))\n        assert \"500\" in text_content.text\n        assert \"internal server error\" in text_content.text.lower()\n\n\n@pytest.mark.asyncio\nasync def test_error_handling(lowlevel_server_simple_app: Server):\n    async with create_connected_server_and_client_session(lowlevel_server_simple_app) as client_session:\n        response = await client_session.call_tool(\"get_item\", {})\n\n        assert response.isError\n        assert len(response.content) > 0\n\n        text_content = next(c for c in response.content if isinstance(c, types.TextContent))\n        assert \"item_id\" in text_content.text.lower() or \"missing\" in text_content.text.lower()\n        assert \"input validation error\" in text_content.text.lower(), \"Expected an input validation error\"\n\n\n@pytest.mark.asyncio\nasync def test_complex_tool_arguments(lowlevel_server_simple_app: Server):\n    test_item = {\n        \"id\": 42,\n        \"name\": \"Test Item\",\n        \"description\": \"A test item for MCP\",\n        \"price\": 9.99,\n        \"tags\": [\"test\", \"mcp\"],\n    }\n\n    async with create_connected_server_and_client_session(lowlevel_server_simple_app) as client_session:\n        response = await client_session.call_tool(\"create_item\", test_item)\n\n        assert not response.isError\n        assert len(response.content) > 0\n\n        text_content = next(c for c in response.content if isinstance(c, types.TextContent))\n        result = json.loads(text_content.text)\n\n        assert result[\"id\"] == test_item[\"id\"]\n        assert result[\"name\"] == test_item[\"name\"]\n        assert result[\"price\"] == test_item[\"price\"]\n        assert result[\"tags\"] == test_item[\"tags\"]\n\n\n@pytest.mark.asyncio\nasync def test_call_tool_list_items_default(lowlevel_server_simple_app: Server):\n    async with create_connected_server_and_client_session(lowlevel_server_simple_app) as client_session:\n        response = await client_session.call_tool(\"list_items\", {})\n\n        assert not response.isError\n        assert len(response.content) > 0\n\n        text_content = next(c for c in response.content if isinstance(c, types.TextContent))\n        results = json.loads(text_content.text)\n        assert len(results) == 3  # Default should return all three items with default pagination\n\n        # Check first item matches expected data\n        item = results[0]\n        assert item[\"id\"] == 1\n        assert item[\"name\"] == \"Item 1\"\n\n\n@pytest.mark.asyncio\nasync def test_call_tool_list_items_with_pagination(lowlevel_server_simple_app: Server):\n    async with create_connected_server_and_client_session(lowlevel_server_simple_app) as client_session:\n        response = await client_session.call_tool(\"list_items\", {\"skip\": 1, \"limit\": 1})\n\n        assert not response.isError\n        assert len(response.content) > 0\n\n        text_content = next(c for c in response.content if isinstance(c, types.TextContent))\n        results = json.loads(text_content.text)\n        assert len(results) == 1\n\n        # Should be the second item in the list (after skipping the first)\n        item = results[0]\n        assert item[\"id\"] == 2\n        assert item[\"name\"] == \"Item 2\"\n\n\n@pytest.mark.asyncio\nasync def test_call_tool_get_item_not_found(lowlevel_server_simple_app: Server):\n    async with create_connected_server_and_client_session(lowlevel_server_simple_app) as client_session:\n        response = await client_session.call_tool(\"get_item\", {\"item_id\": 999})\n\n        assert response.isError\n        assert len(response.content) > 0\n\n        text_content = next(c for c in response.content if isinstance(c, types.TextContent))\n        assert \"404\" in text_content.text\n        assert \"not found\" in text_content.text.lower()\n\n\n@pytest.mark.asyncio\nasync def test_call_tool_update_item(lowlevel_server_simple_app: Server):\n    test_update = {\n        \"item_id\": 3,\n        \"id\": 3,\n        \"name\": \"Updated Item 3\",\n        \"description\": \"Updated description\",\n        \"price\": 35.99,\n        \"tags\": [\"updated\", \"modified\"],\n    }\n\n    async with create_connected_server_and_client_session(lowlevel_server_simple_app) as client_session:\n        response = await client_session.call_tool(\"update_item\", test_update)\n\n        assert not response.isError\n        assert len(response.content) > 0\n\n        text_content = next(c for c in response.content if isinstance(c, types.TextContent))\n        result = json.loads(text_content.text)\n\n        assert result[\"id\"] == test_update[\"item_id\"]\n        assert result[\"name\"] == test_update[\"name\"]\n        assert result[\"description\"] == test_update[\"description\"]\n        assert result[\"price\"] == test_update[\"price\"]\n        assert result[\"tags\"] == test_update[\"tags\"]\n\n\n@pytest.mark.asyncio\nasync def test_call_tool_delete_item(lowlevel_server_simple_app: Server):\n    async with create_connected_server_and_client_session(lowlevel_server_simple_app) as client_session:\n        response = await client_session.call_tool(\"delete_item\", {\"item_id\": 3})\n\n        assert not response.isError\n        # The endpoint returns 204 No Content, so we expect an empty response\n        text_content = next(c for c in response.content if isinstance(c, types.TextContent))\n        assert (\n            text_content.text.strip() == \"{}\" or text_content.text.strip() == \"null\" or text_content.text.strip() == \"\"\n        )\n\n\n@pytest.mark.asyncio\nasync def test_call_tool_get_item_with_details(lowlevel_server_simple_app: Server):\n    async with create_connected_server_and_client_session(lowlevel_server_simple_app) as client_session:\n        response = await client_session.call_tool(\"get_item\", {\"item_id\": 1, \"include_details\": True})\n\n        assert not response.isError\n        assert len(response.content) > 0\n\n        text_content = next(c for c in response.content if isinstance(c, types.TextContent))\n        result: dict = json.loads(text_content.text)\n        parsed_result = Item(**result)\n\n        assert parsed_result.id == 1\n        assert parsed_result.name == \"Item 1\"\n        assert parsed_result.price == 10.0\n        assert parsed_result.tags == [\"tag1\", \"tag2\"]\n        assert parsed_result.description == \"Item 1 description\"\n\n\n@pytest.mark.asyncio\nasync def test_headers_passthrough_to_tool_handler(fastapi_mcp: FastApiMCP):\n    \"\"\"Test that the original request's headers pass through to the MCP tool call handler.\"\"\"\n    from unittest.mock import patch, MagicMock\n    from fastapi_mcp.types import HTTPRequestInfo\n\n    # Test with uppercase \"Authorization\" header\n    with patch.object(fastapi_mcp, \"_request\") as mock_request:\n        mock_response = MagicMock()\n        mock_response.status_code = 200\n        mock_response.text = '{\"result\": \"success\"}'\n        mock_response.json.return_value = {\"result\": \"success\"}\n        mock_request.return_value = mock_response\n\n        http_request_info = HTTPRequestInfo(\n            method=\"POST\",\n            path=\"/test\",\n            headers={\"Authorization\": \"Bearer token123\"},\n            cookies={},\n            query_params={},\n            body=None,\n        )\n\n        try:\n            # Call the _execute_api_tool method directly\n            # We don't care if it succeeds, just that _request gets the right headers\n            await fastapi_mcp._execute_api_tool(\n                client=fastapi_mcp._http_client,\n                tool_name=\"get_item\",\n                arguments={\"item_id\": 1},\n                operation_map=fastapi_mcp.operation_map,\n                http_request_info=http_request_info,\n            )\n        except Exception:\n            pass\n\n        assert mock_request.called, \"The _request method was not called\"\n\n        if mock_request.called:\n            headers_arg = mock_request.call_args[0][4]  # headers are the 5th argument\n            assert \"Authorization\" in headers_arg\n            assert headers_arg[\"Authorization\"] == \"Bearer token123\"\n\n    # Test again with lowercase \"authorization\" header\n    with patch.object(fastapi_mcp, \"_request\") as mock_request:\n        mock_response = MagicMock()\n        mock_response.status_code = 200\n        mock_response.text = '{\"result\": \"success\"}'\n        mock_response.json.return_value = {\"result\": \"success\"}\n        mock_request.return_value = mock_response\n\n        http_request_info = HTTPRequestInfo(\n            method=\"POST\",\n            path=\"/test\",\n            headers={\"authorization\": \"Bearer token456\"},\n            cookies={},\n            query_params={},\n            body=None,\n        )\n\n        try:\n            await fastapi_mcp._execute_api_tool(\n                client=fastapi_mcp._http_client,\n                tool_name=\"get_item\",\n                arguments={\"item_id\": 1},\n                operation_map=fastapi_mcp.operation_map,\n                http_request_info=http_request_info,\n            )\n        except Exception:\n            pass\n\n        assert mock_request.called, \"The _request method was not called\"\n\n        if mock_request.called:\n            headers_arg = mock_request.call_args[0][4]  # headers are the 5th argument\n            assert \"authorization\" in headers_arg\n            assert headers_arg[\"authorization\"] == \"Bearer token456\"\n\n\n@pytest.mark.asyncio\nasync def test_custom_header_passthrough_to_tool_handler(fastapi_mcp_with_custom_header: FastApiMCP):\n    from unittest.mock import patch, MagicMock\n    from fastapi_mcp.types import HTTPRequestInfo\n\n    # Test with custom header \"X-Custom-Header\"\n    with patch.object(fastapi_mcp_with_custom_header, \"_request\") as mock_request:\n        mock_response = MagicMock()\n        mock_response.status_code = 200\n        mock_response.text = '{\"result\": \"success\"}'\n        mock_response.json.return_value = {\"result\": \"success\"}\n        mock_request.return_value = mock_response\n\n        http_request_info = HTTPRequestInfo(\n            method=\"POST\",\n            path=\"/test\",\n            headers={\"X-Custom-Header\": \"MyValue123\"},\n            cookies={},\n            query_params={},\n            body=None,\n        )\n\n        try:\n            # Call the _execute_api_tool method directly\n            # We don't care if it succeeds, just that _request gets the right headers\n            await fastapi_mcp_with_custom_header._execute_api_tool(\n                client=fastapi_mcp_with_custom_header._http_client,\n                tool_name=\"get_item\",\n                arguments={\"item_id\": 1},\n                operation_map=fastapi_mcp_with_custom_header.operation_map,\n                http_request_info=http_request_info,\n            )\n        except Exception:\n            pass\n\n        assert mock_request.called, \"The _request method was not called\"\n\n        if mock_request.called:\n            headers_arg = mock_request.call_args[0][4]  # headers are the 5th argument\n            assert \"X-Custom-Header\" in headers_arg\n            assert headers_arg[\"X-Custom-Header\"] == \"MyValue123\"\n\n\n@pytest.mark.asyncio\nasync def test_context_extraction_in_tool_handler(fastapi_mcp: FastApiMCP):\n    \"\"\"Test that handle_call_tool extracts HTTP request info from MCP context.\"\"\"\n    from unittest.mock import patch, MagicMock\n    import mcp.types as types\n    from mcp.server.lowlevel.server import request_ctx\n\n    # Create a fake HTTP request object with headers\n    fake_http_request = MagicMock()\n    fake_http_request.method = \"POST\"\n    fake_http_request.url.path = \"/test\"\n    fake_http_request.headers = {\"Authorization\": \"Bearer token-123\", \"X-Custom\": \"custom-value-123\"}\n    fake_http_request.cookies = {}\n    fake_http_request.query_params = {}\n\n    # Create a fake request context containing the HTTP request\n    fake_request_context = MagicMock()\n    fake_request_context.request = fake_http_request\n\n    # Test with authorization header extraction from context\n    token = request_ctx.set(fake_request_context)\n    try:\n        with patch.object(fastapi_mcp, \"_execute_api_tool\") as mock_execute:\n            mock_execute.return_value = [types.TextContent(type=\"text\", text=\"success\")]\n\n            # Create a CallToolRequest like the MCP protocol would\n            call_request = types.CallToolRequest(\n                method=\"tools/call\", params=types.CallToolRequestParams(name=\"get_item\", arguments={\"item_id\": 1})\n            )\n\n            try:\n                # Call the tool handler directly like the MCP server would\n                await fastapi_mcp.server.request_handlers[types.CallToolRequest](call_request)\n            except Exception:\n                pass\n\n            assert mock_execute.called, \"The _execute_api_tool method was not called\"\n\n            if mock_execute.called:\n                # Verify that HTTPRequestInfo was extracted from context and passed to _execute_api_tool\n                http_request_info = mock_execute.call_args.kwargs[\"http_request_info\"]\n                assert http_request_info is not None, \"HTTPRequestInfo should be extracted from context\"\n                assert http_request_info.method == \"POST\"\n                assert http_request_info.path == \"/test\"\n                assert \"Authorization\" in http_request_info.headers\n                assert http_request_info.headers[\"Authorization\"] == \"Bearer token-123\"\n                assert \"X-Custom\" in http_request_info.headers\n                assert http_request_info.headers[\"X-Custom\"] == \"custom-value-123\"\n    finally:\n        # Clean up the context variable\n        request_ctx.reset(token)\n\n    # Test with missing request context (should still work but with None)\n    with patch.object(fastapi_mcp, \"_execute_api_tool\") as mock_execute:\n        mock_execute.return_value = [types.TextContent(type=\"text\", text=\"success\")]\n\n        call_request = types.CallToolRequest(\n            method=\"tools/call\", params=types.CallToolRequestParams(name=\"get_item\", arguments={\"item_id\": 1})\n        )\n\n        try:\n            await fastapi_mcp.server.request_handlers[types.CallToolRequest](call_request)\n        except Exception:\n            pass\n\n        assert mock_execute.called, \"The _execute_api_tool method was not called\"\n\n        if mock_execute.called:\n            # Verify that HTTPRequestInfo is None when context is not available\n            http_request_info = mock_execute.call_args.kwargs[\"http_request_info\"]\n            assert http_request_info is None, \"HTTPRequestInfo should be None when context is not available\"\n"
  },
  {
    "path": "tests/test_openapi_conversion.py",
    "content": "from fastapi import FastAPI\nfrom fastapi.openapi.utils import get_openapi\nimport mcp.types as types\n\nfrom fastapi_mcp.openapi.convert import convert_openapi_to_mcp_tools\nfrom fastapi_mcp.openapi.utils import (\n    clean_schema_for_display,\n    generate_example_from_schema,\n    get_single_param_type_from_schema,\n)\n\n\ndef test_simple_app_conversion(simple_fastapi_app: FastAPI):\n    openapi_schema = get_openapi(\n        title=simple_fastapi_app.title,\n        version=simple_fastapi_app.version,\n        openapi_version=simple_fastapi_app.openapi_version,\n        description=simple_fastapi_app.description,\n        routes=simple_fastapi_app.routes,\n    )\n\n    tools, operation_map = convert_openapi_to_mcp_tools(openapi_schema)\n\n    assert len(tools) == 6\n    assert len(operation_map) == 6\n\n    expected_operations = [\"list_items\", \"get_item\", \"create_item\", \"update_item\", \"delete_item\", \"raise_error\"]\n    for op in expected_operations:\n        assert op in operation_map\n\n    for tool in tools:\n        assert isinstance(tool, types.Tool)\n        assert tool.name in expected_operations\n        assert tool.description is not None\n        assert tool.inputSchema is not None\n\n\ndef test_complex_app_conversion(complex_fastapi_app: FastAPI):\n    openapi_schema = get_openapi(\n        title=complex_fastapi_app.title,\n        version=complex_fastapi_app.version,\n        openapi_version=complex_fastapi_app.openapi_version,\n        description=complex_fastapi_app.description,\n        routes=complex_fastapi_app.routes,\n    )\n\n    tools, operation_map = convert_openapi_to_mcp_tools(openapi_schema)\n\n    expected_operations = [\"list_products\", \"get_product\", \"create_order\", \"get_customer\"]\n    assert len(tools) == len(expected_operations)\n    assert len(operation_map) == len(expected_operations)\n\n    for op in expected_operations:\n        assert op in operation_map\n\n    for tool in tools:\n        assert isinstance(tool, types.Tool)\n        assert tool.name in expected_operations\n        assert tool.description is not None\n        assert tool.inputSchema is not None\n\n\ndef test_describe_full_response_schema(simple_fastapi_app: FastAPI):\n    openapi_schema = get_openapi(\n        title=simple_fastapi_app.title,\n        version=simple_fastapi_app.version,\n        openapi_version=simple_fastapi_app.openapi_version,\n        description=simple_fastapi_app.description,\n        routes=simple_fastapi_app.routes,\n    )\n\n    tools_full, _ = convert_openapi_to_mcp_tools(openapi_schema, describe_full_response_schema=True)\n\n    tools_simple, _ = convert_openapi_to_mcp_tools(openapi_schema, describe_full_response_schema=False)\n\n    for i, tool in enumerate(tools_full):\n        assert tool.description is not None\n        assert tools_simple[i].description is not None\n\n        tool_desc = tool.description or \"\"\n        simple_desc = tools_simple[i].description or \"\"\n\n        assert len(tool_desc) >= len(simple_desc)\n\n        if tool.name == \"delete_item\":\n            continue\n\n        assert \"**Output Schema:**\" in tool_desc\n\n        if \"**Output Schema:**\" in simple_desc:\n            assert len(tool_desc) > len(simple_desc)\n\n\ndef test_describe_all_responses(complex_fastapi_app: FastAPI):\n    openapi_schema = get_openapi(\n        title=complex_fastapi_app.title,\n        version=complex_fastapi_app.version,\n        openapi_version=complex_fastapi_app.openapi_version,\n        description=complex_fastapi_app.description,\n        routes=complex_fastapi_app.routes,\n    )\n\n    tools_all, _ = convert_openapi_to_mcp_tools(openapi_schema, describe_all_responses=True)\n\n    tools_success, _ = convert_openapi_to_mcp_tools(openapi_schema, describe_all_responses=False)\n\n    create_order_all = next(t for t in tools_all if t.name == \"create_order\")\n    create_order_success = next(t for t in tools_success if t.name == \"create_order\")\n\n    assert create_order_all.description is not None\n    assert create_order_success.description is not None\n\n    all_desc = create_order_all.description or \"\"\n    success_desc = create_order_success.description or \"\"\n\n    assert \"400\" in all_desc\n    assert \"404\" in all_desc\n    assert \"422\" in all_desc\n\n    assert all_desc.count(\"400\") >= success_desc.count(\"400\")\n\n\ndef test_schema_utils():\n    schema = {\n        \"type\": \"object\",\n        \"properties\": {\n            \"id\": {\"type\": \"integer\"},\n            \"name\": {\"type\": \"string\"},\n            \"tags\": {\"type\": \"array\", \"items\": {\"type\": \"string\"}},\n        },\n        \"required\": [\"id\", \"name\"],\n        \"additionalProperties\": False,\n        \"x-internal\": \"Some internal data\",\n    }\n\n    cleaned = clean_schema_for_display(schema)\n\n    assert \"required\" in cleaned\n    assert \"properties\" in cleaned\n    assert \"type\" in cleaned\n\n    example = generate_example_from_schema(schema)\n    assert \"id\" in example\n    assert \"name\" in example\n    assert \"tags\" in example\n    assert isinstance(example[\"id\"], int)\n    assert isinstance(example[\"name\"], str)\n    assert isinstance(example[\"tags\"], list)\n\n    assert get_single_param_type_from_schema({\"type\": \"string\"}) == \"string\"\n    assert get_single_param_type_from_schema({\"type\": \"array\", \"items\": {\"type\": \"string\"}}) == \"array\"\n\n    array_schema = {\"type\": \"array\", \"items\": {\"type\": \"string\", \"enum\": [\"red\", \"green\", \"blue\"]}}\n    array_example = generate_example_from_schema(array_schema)\n    assert isinstance(array_example, list)\n    assert len(array_example) > 0\n\n    assert isinstance(array_example[0], str)\n\n\ndef test_parameter_handling(complex_fastapi_app: FastAPI):\n    openapi_schema = get_openapi(\n        title=complex_fastapi_app.title,\n        version=complex_fastapi_app.version,\n        openapi_version=complex_fastapi_app.openapi_version,\n        description=complex_fastapi_app.description,\n        routes=complex_fastapi_app.routes,\n    )\n\n    tools, operation_map = convert_openapi_to_mcp_tools(openapi_schema)\n\n    list_products_tool = next(tool for tool in tools if tool.name == \"list_products\")\n\n    properties = list_products_tool.inputSchema[\"properties\"]\n\n    assert \"product_id\" not in properties  # This is from get_product, not list_products\n\n    assert \"category\" in properties\n    assert properties[\"category\"].get(\"type\") == \"string\"  # Enum converted to string\n    assert \"description\" in properties[\"category\"]\n    assert \"Filter by product category\" in properties[\"category\"][\"description\"]\n\n    assert \"min_price\" in properties\n    assert properties[\"min_price\"].get(\"type\") == \"number\"\n    assert \"description\" in properties[\"min_price\"]\n    assert \"Minimum price filter\" in properties[\"min_price\"][\"description\"]\n    if \"minimum\" in properties[\"min_price\"]:\n        assert properties[\"min_price\"][\"minimum\"] > 0  # gt=0 in Query param\n\n    assert \"in_stock_only\" in properties\n    assert properties[\"in_stock_only\"].get(\"type\") == \"boolean\"\n    assert properties[\"in_stock_only\"].get(\"default\") is False  # Default value preserved\n\n    assert \"page\" in properties\n    assert properties[\"page\"].get(\"type\") == \"integer\"\n    assert properties[\"page\"].get(\"default\") == 1  # Default value preserved\n    if \"minimum\" in properties[\"page\"]:\n        assert properties[\"page\"][\"minimum\"] >= 1  # ge=1 in Query param\n\n    assert \"size\" in properties\n    assert properties[\"size\"].get(\"type\") == \"integer\"\n    if \"minimum\" in properties[\"size\"] and \"maximum\" in properties[\"size\"]:\n        assert properties[\"size\"][\"minimum\"] >= 1  # ge=1 in Query param\n        assert properties[\"size\"][\"maximum\"] <= 100  # le=100 in Query param\n\n    assert \"tag\" in properties\n    assert properties[\"tag\"].get(\"type\") == \"array\"\n\n    required = list_products_tool.inputSchema.get(\"required\", [])\n    assert \"page\" not in required  # Has default value\n    assert \"category\" not in required  # Optional parameter\n\n    assert \"list_products\" in operation_map\n    assert operation_map[\"list_products\"][\"path\"] == \"/products\"\n    assert operation_map[\"list_products\"][\"method\"] == \"get\"\n\n    get_product_tool = next(tool for tool in tools if tool.name == \"get_product\")\n    get_product_props = get_product_tool.inputSchema[\"properties\"]\n\n    assert \"product_id\" in get_product_props\n    assert get_product_props[\"product_id\"].get(\"type\") == \"string\"  # UUID converted to string\n    assert \"description\" in get_product_props[\"product_id\"]\n\n    get_customer_tool = next(tool for tool in tools if tool.name == \"get_customer\")\n    get_customer_props = get_customer_tool.inputSchema[\"properties\"]\n\n    assert \"fields\" in get_customer_props\n    assert get_customer_props[\"fields\"].get(\"type\") == \"array\"\n    if \"items\" in get_customer_props[\"fields\"]:\n        assert get_customer_props[\"fields\"][\"items\"].get(\"type\") == \"string\"\n\n\ndef test_request_body_handling(complex_fastapi_app: FastAPI):\n    openapi_schema = get_openapi(\n        title=complex_fastapi_app.title,\n        version=complex_fastapi_app.version,\n        openapi_version=complex_fastapi_app.openapi_version,\n        description=complex_fastapi_app.description,\n        routes=complex_fastapi_app.routes,\n    )\n\n    create_order_route = openapi_schema[\"paths\"][\"/orders\"][\"post\"]\n    original_request_body = create_order_route[\"requestBody\"][\"content\"][\"application/json\"][\"schema\"]\n    original_properties = original_request_body.get(\"properties\", {})\n\n    tools, operation_map = convert_openapi_to_mcp_tools(openapi_schema)\n\n    create_order_tool = next(tool for tool in tools if tool.name == \"create_order\")\n\n    properties = create_order_tool.inputSchema[\"properties\"]\n\n    assert \"customer_id\" in properties\n    assert \"items\" in properties\n    assert \"shipping_address_id\" in properties\n    assert \"payment_method\" in properties\n    assert \"notes\" in properties\n\n    for param_name in [\"customer_id\", \"items\", \"shipping_address_id\", \"payment_method\", \"notes\"]:\n        if \"description\" in original_properties.get(param_name, {}):\n            assert \"description\" in properties[param_name]\n            assert properties[param_name][\"description\"] == original_properties[param_name][\"description\"]\n\n    for param_name in [\"customer_id\", \"items\", \"shipping_address_id\", \"payment_method\", \"notes\"]:\n        assert properties[param_name][\"title\"] == param_name\n\n    for param_name in [\"customer_id\", \"items\", \"shipping_address_id\", \"payment_method\", \"notes\"]:\n        if \"default\" in original_properties.get(param_name, {}):\n            assert \"default\" in properties[param_name]\n            assert properties[param_name][\"default\"] == original_properties[param_name][\"default\"]\n\n    required = create_order_tool.inputSchema.get(\"required\", [])\n    assert \"customer_id\" in required\n    assert \"items\" in required\n    assert \"shipping_address_id\" in required\n    assert \"payment_method\" in required\n    assert \"notes\" not in required  # Optional in OrderRequest\n\n    assert properties[\"items\"].get(\"type\") == \"array\"\n    if \"items\" in properties[\"items\"]:\n        item_props = properties[\"items\"][\"items\"]\n        assert item_props.get(\"type\") == \"object\"\n        if \"properties\" in item_props:\n            assert \"product_id\" in item_props[\"properties\"]\n            assert \"quantity\" in item_props[\"properties\"]\n            assert \"unit_price\" in item_props[\"properties\"]\n            assert \"total\" in item_props[\"properties\"]\n\n            for nested_param in [\"product_id\", \"quantity\", \"unit_price\", \"total\"]:\n                assert \"title\" in item_props[\"properties\"][nested_param]\n\n                # Check if the original nested schema had descriptions\n                original_item_schema = original_properties.get(\"items\", {}).get(\"items\", {}).get(\"properties\", {})\n                if \"description\" in original_item_schema.get(nested_param, {}):\n                    assert \"description\" in item_props[\"properties\"][nested_param]\n                    assert (\n                        item_props[\"properties\"][nested_param][\"description\"]\n                        == original_item_schema[nested_param][\"description\"]\n                    )\n\n    assert \"create_order\" in operation_map\n    assert operation_map[\"create_order\"][\"path\"] == \"/orders\"\n    assert operation_map[\"create_order\"][\"method\"] == \"post\"\n\n\ndef test_missing_type_handling(complex_fastapi_app: FastAPI):\n    openapi_schema = get_openapi(\n        title=complex_fastapi_app.title,\n        version=complex_fastapi_app.version,\n        openapi_version=complex_fastapi_app.openapi_version,\n        description=complex_fastapi_app.description,\n        routes=complex_fastapi_app.routes,\n    )\n\n    # Remove the type field from the product_id schema\n    params = openapi_schema[\"paths\"][\"/products/{product_id}\"][\"get\"][\"parameters\"]\n    for param in params:\n        if param.get(\"name\") == \"product_id\" and \"schema\" in param:\n            param[\"schema\"].pop(\"type\", None)\n            break\n\n    tools, operation_map = convert_openapi_to_mcp_tools(openapi_schema)\n\n    get_product_tool = next(tool for tool in tools if tool.name == \"get_product\")\n    get_product_props = get_product_tool.inputSchema[\"properties\"]\n\n    assert \"product_id\" in get_product_props\n    assert get_product_props[\"product_id\"].get(\"type\") == \"string\"  # Default type applied\n\n\ndef test_body_params_descriptions_and_defaults(complex_fastapi_app: FastAPI):\n    \"\"\"\n    Test that descriptions and defaults from request body parameters\n    are properly transferred to the MCP tool schema properties.\n    \"\"\"\n    openapi_schema = get_openapi(\n        title=complex_fastapi_app.title,\n        version=complex_fastapi_app.version,\n        openapi_version=complex_fastapi_app.openapi_version,\n        description=complex_fastapi_app.description,\n        routes=complex_fastapi_app.routes,\n    )\n\n    order_request_schema = openapi_schema[\"components\"][\"schemas\"][\"OrderRequest\"]\n\n    order_request_schema[\"properties\"][\"customer_id\"][\"description\"] = \"Test customer ID description\"\n    order_request_schema[\"properties\"][\"payment_method\"][\"description\"] = \"Test payment method description\"\n    order_request_schema[\"properties\"][\"notes\"][\"default\"] = \"Default order notes\"\n\n    item_schema = openapi_schema[\"components\"][\"schemas\"][\"OrderItem\"]\n    item_schema[\"properties\"][\"product_id\"][\"description\"] = \"Test product ID description\"\n    item_schema[\"properties\"][\"quantity\"][\"default\"] = 1\n\n    tools, _ = convert_openapi_to_mcp_tools(openapi_schema)\n\n    create_order_tool = next(tool for tool in tools if tool.name == \"create_order\")\n    properties = create_order_tool.inputSchema[\"properties\"]\n\n    assert \"description\" in properties[\"customer_id\"]\n    assert properties[\"customer_id\"][\"description\"] == \"Test customer ID description\"\n\n    assert \"description\" in properties[\"payment_method\"]\n    assert properties[\"payment_method\"][\"description\"] == \"Test payment method description\"\n\n    assert \"default\" in properties[\"notes\"]\n    assert properties[\"notes\"][\"default\"] == \"Default order notes\"\n\n    if \"items\" in properties:\n        assert properties[\"items\"][\"type\"] == \"array\"\n        assert \"items\" in properties[\"items\"]\n\n        item_props = properties[\"items\"][\"items\"][\"properties\"]\n\n        assert \"description\" in item_props[\"product_id\"]\n        assert item_props[\"product_id\"][\"description\"] == \"Test product ID description\"\n\n        assert \"default\" in item_props[\"quantity\"]\n        assert item_props[\"quantity\"][\"default\"] == 1\n\n\ndef test_body_params_edge_cases(complex_fastapi_app: FastAPI):\n    \"\"\"\n    Test handling of edge cases for body parameters, such as:\n    - Empty or missing descriptions\n    - Missing type information\n    - Empty properties object\n    - Schema without properties\n    \"\"\"\n    openapi_schema = get_openapi(\n        title=complex_fastapi_app.title,\n        version=complex_fastapi_app.version,\n        openapi_version=complex_fastapi_app.openapi_version,\n        description=complex_fastapi_app.description,\n        routes=complex_fastapi_app.routes,\n    )\n\n    order_request_schema = openapi_schema[\"components\"][\"schemas\"][\"OrderRequest\"]\n\n    if \"description\" in order_request_schema[\"properties\"][\"customer_id\"]:\n        del order_request_schema[\"properties\"][\"customer_id\"][\"description\"]\n\n    if \"type\" in order_request_schema[\"properties\"][\"notes\"]:\n        del order_request_schema[\"properties\"][\"notes\"][\"type\"]\n\n    item_schema = openapi_schema[\"components\"][\"schemas\"][\"OrderItem\"]\n\n    if \"properties\" in item_schema[\"properties\"][\"total\"]:\n        del item_schema[\"properties\"][\"total\"][\"properties\"]\n\n    tools, _ = convert_openapi_to_mcp_tools(openapi_schema)\n\n    create_order_tool = next(tool for tool in tools if tool.name == \"create_order\")\n    properties = create_order_tool.inputSchema[\"properties\"]\n\n    assert \"customer_id\" in properties\n    assert \"title\" in properties[\"customer_id\"]\n    assert properties[\"customer_id\"][\"title\"] == \"customer_id\"\n\n    assert \"notes\" in properties\n    assert \"type\" in properties[\"notes\"]\n    assert properties[\"notes\"][\"type\"] in [\"string\", \"object\"]  # Default should be either string or object\n\n    if \"items\" in properties:\n        item_props = properties[\"items\"][\"items\"][\"properties\"]\n        assert \"total\" in item_props\n"
  },
  {
    "path": "tests/test_sse_mock_transport.py",
    "content": "import pytest\nimport uuid\nfrom uuid import UUID\nfrom unittest.mock import AsyncMock, MagicMock, patch\nfrom fastapi import HTTPException, Request\nfrom mcp.shared.message import SessionMessage\nfrom pydantic import ValidationError\nfrom anyio.streams.memory import MemoryObjectSendStream\n\nfrom fastapi_mcp.transport.sse import FastApiSseTransport\nfrom mcp.types import JSONRPCMessage, JSONRPCError\n\n\n@pytest.fixture\ndef mock_transport() -> FastApiSseTransport:\n    # Initialize transport with a mock endpoint\n    transport = FastApiSseTransport(\"/messages\")\n    transport._read_stream_writers = {}\n    return transport\n\n\n@pytest.fixture\ndef valid_session_id():\n    session_id = uuid.uuid4()\n    return session_id\n\n\n@pytest.fixture\ndef mock_writer():\n    return AsyncMock(spec=MemoryObjectSendStream)\n\n\n@pytest.mark.anyio\nasync def test_handle_post_message_missing_session_id(mock_transport: FastApiSseTransport) -> None:\n    \"\"\"Test handling a request with a missing session_id.\"\"\"\n    # Create a mock request with no session_id\n    mock_request = MagicMock(spec=Request)\n    mock_request.query_params = {}\n\n    # Check that the function raises HTTPException with the correct status code\n    with pytest.raises(HTTPException) as excinfo:\n        await mock_transport.handle_fastapi_post_message(mock_request)\n\n    assert excinfo.value.status_code == 400\n    assert \"session_id is required\" in excinfo.value.detail\n\n\n@pytest.mark.anyio\nasync def test_handle_post_message_invalid_session_id(mock_transport: FastApiSseTransport) -> None:\n    \"\"\"Test handling a request with an invalid session_id.\"\"\"\n    # Create a mock request with an invalid session_id\n    mock_request = MagicMock(spec=Request)\n    mock_request.query_params = {\"session_id\": \"not-a-valid-uuid\"}\n\n    # Check that the function raises HTTPException with the correct status code\n    with pytest.raises(HTTPException) as excinfo:\n        await mock_transport.handle_fastapi_post_message(mock_request)\n\n    assert excinfo.value.status_code == 400\n    assert \"Invalid session ID\" in excinfo.value.detail\n\n\n@pytest.mark.anyio\nasync def test_handle_post_message_session_not_found(\n    mock_transport: FastApiSseTransport, valid_session_id: UUID\n) -> None:\n    \"\"\"Test handling a request with a valid session_id that doesn't exist.\"\"\"\n    # Create a mock request with a valid session_id\n    mock_request = MagicMock(spec=Request)\n    mock_request.query_params = {\"session_id\": valid_session_id.hex}\n\n    # The session_id is valid but not in the transport's writers\n    with pytest.raises(HTTPException) as excinfo:\n        await mock_transport.handle_fastapi_post_message(mock_request)\n\n    assert excinfo.value.status_code == 404\n    assert \"Could not find session\" in excinfo.value.detail\n\n\n@pytest.mark.anyio\nasync def test_handle_post_message_validation_error(\n    mock_transport: FastApiSseTransport, valid_session_id: UUID, mock_writer: AsyncMock\n) -> None:\n    \"\"\"Test handling a request with invalid JSON that causes a ValidationError.\"\"\"\n    # Set up the mock transport with a valid session\n    mock_transport._read_stream_writers[valid_session_id] = mock_writer\n\n    # Create a mock request with valid session_id but invalid body\n    mock_request = MagicMock(spec=Request)\n    mock_request.query_params = {\"session_id\": valid_session_id.hex}\n    mock_request.body = AsyncMock(return_value=b'{\"invalid\": \"json\"}')\n\n    # Mock BackgroundTasks\n    with patch(\"fastapi_mcp.transport.sse.BackgroundTasks\") as MockBackgroundTasks:\n        mock_background_tasks = MockBackgroundTasks.return_value\n\n        # Call the function\n        response = await mock_transport.handle_fastapi_post_message(mock_request)\n\n        # Verify response and background task setup\n        assert response.status_code == 400\n        assert \"error\" in response.body.decode() if isinstance(response.body, bytes) else False\n        assert mock_background_tasks.add_task.called\n        assert response.background == mock_background_tasks\n\n\n@pytest.mark.anyio\nasync def test_handle_post_message_general_exception(\n    mock_transport: FastApiSseTransport, valid_session_id: UUID, mock_writer: AsyncMock\n) -> None:\n    \"\"\"Test handling a request that causes a general exception during body processing.\"\"\"\n    # Set up the mock transport with a valid session\n    mock_transport._read_stream_writers[valid_session_id] = mock_writer\n\n    # Create a mock request that raises an exception when body is accessed\n    mock_request = MagicMock(spec=Request)\n    mock_request.query_params = {\"session_id\": valid_session_id.hex}\n\n    # Instead of mocking the body method to raise an exception,\n    # we'll patch the body method to return a normal value and then\n    # patch JSONRPCMessage.model_validate_json to raise the exception\n    mock_request.body = AsyncMock(return_value=b'{\"jsonrpc\": \"2.0\", \"method\": \"test\", \"id\": \"1\"}')\n\n    # Mock the model_validate_json method to raise an Exception\n    with patch(\"mcp.types.JSONRPCMessage.model_validate_json\", side_effect=Exception(\"Test exception\")):\n        # Check that the function raises HTTPException with the correct status code\n        with pytest.raises(HTTPException) as excinfo:\n            await mock_transport.handle_fastapi_post_message(mock_request)\n\n        assert excinfo.value.status_code == 400\n        assert \"Invalid request body\" in excinfo.value.detail\n\n\n@pytest.mark.anyio\nasync def test_send_message_safely_with_validation_error(\n    mock_transport: FastApiSseTransport, mock_writer: AsyncMock\n) -> None:\n    \"\"\"Test sending a ValidationError message safely.\"\"\"\n    # Create a minimal validation error manually instead of using from_exception_data\n    mock_validation_error = MagicMock(spec=ValidationError)\n    mock_validation_error.__str__.return_value = \"Mock validation error\"  # type: ignore\n\n    # Call the function\n    await mock_transport._send_message_safely(mock_writer, mock_validation_error)\n\n    # Verify that the writer.send was called with a JSONRPCError\n    assert mock_writer.send.called\n    sent_message = mock_writer.send.call_args[0][0]\n    assert isinstance(sent_message, SessionMessage)\n    assert isinstance(sent_message.message, JSONRPCMessage)\n    assert isinstance(sent_message.message.root, JSONRPCError)\n    assert sent_message.message.root.error.code == -32700  # Parse error code\n\n\n@pytest.mark.anyio\nasync def test_send_message_safely_with_jsonrpc_message(\n    mock_transport: FastApiSseTransport, mock_writer: AsyncMock\n) -> None:\n    \"\"\"Test sending a JSONRPCMessage safely.\"\"\"\n    # Create a JSONRPCMessage\n    message = SessionMessage(\n        JSONRPCMessage.model_validate({\"jsonrpc\": \"2.0\", \"id\": \"123\", \"method\": \"test_method\", \"params\": {}})\n    )\n\n    # Call the function\n    await mock_transport._send_message_safely(mock_writer, message)\n\n    # Verify that the writer.send was called with the message\n    assert mock_writer.send.called\n    sent_message = mock_writer.send.call_args[0][0]\n    assert sent_message == message\n\n\n@pytest.mark.anyio\nasync def test_send_message_safely_exception_handling(\n    mock_transport: FastApiSseTransport, mock_writer: AsyncMock\n) -> None:\n    \"\"\"Test exception handling when sending a message.\"\"\"\n    # Set up the writer to raise an exception\n    mock_writer.send.side_effect = Exception(\"Test exception\")\n\n    # Create a message\n    message = SessionMessage(\n        JSONRPCMessage.model_validate({\"jsonrpc\": \"2.0\", \"id\": \"123\", \"method\": \"test_method\", \"params\": {}})\n    )\n\n    # Call the function - it should not raise an exception\n    await mock_transport._send_message_safely(mock_writer, message)\n\n    # Verify that the writer.send was called\n    assert mock_writer.send.called\n"
  },
  {
    "path": "tests/test_sse_real_transport.py",
    "content": "import anyio\nimport multiprocessing\nimport socket\nimport time\nimport os\nimport signal\nimport atexit\nimport sys\nimport threading\nimport coverage\nfrom typing import AsyncGenerator, Generator\nfrom fastapi import FastAPI\nfrom mcp.client.session import ClientSession\nfrom mcp.client.sse import sse_client\nfrom mcp import InitializeResult\nfrom mcp.types import EmptyResult, CallToolResult, ListToolsResult\nimport pytest\nimport httpx\nimport uvicorn\nfrom fastapi_mcp import FastApiMCP\n\n\nHOST = \"127.0.0.1\"\nSERVER_NAME = \"Test MCP Server\"\n\n\ndef run_server(server_port: int, fastapi_app: FastAPI) -> None:\n    # Initialize coverage for subprocesses\n    cov = None\n    if \"COVERAGE_PROCESS_START\" in os.environ:\n        cov = coverage.Coverage(source=[\"fastapi_mcp\"])\n        cov.start()\n\n        # Create a function to save coverage data at exit\n        def cleanup():\n            if cov:\n                cov.stop()\n                cov.save()\n\n        # Register multiple cleanup mechanisms to ensure coverage data is saved\n        atexit.register(cleanup)\n\n        # Setup signal handler for clean termination\n        def handle_signal(signum, frame):\n            cleanup()\n            sys.exit(0)\n\n        signal.signal(signal.SIGTERM, handle_signal)\n\n        # Backup thread to ensure coverage is written if process is terminated abruptly\n        def periodic_save():\n            while True:\n                time.sleep(1.0)\n                if cov:\n                    cov.save()\n\n        save_thread = threading.Thread(target=periodic_save)\n        save_thread.daemon = True\n        save_thread.start()\n\n    # Configure the server\n    mcp = FastApiMCP(\n        fastapi_app,\n        name=SERVER_NAME,\n        description=\"Test description\",\n    )\n    mcp.mount_sse()\n\n    # Start the server\n    server = uvicorn.Server(config=uvicorn.Config(app=fastapi_app, host=HOST, port=server_port, log_level=\"error\"))\n    server.run()\n\n    # Give server time to start\n    while not server.started:\n        time.sleep(0.5)\n\n    # Ensure coverage is saved if exiting the normal way\n    if cov:\n        cov.stop()\n        cov.save()\n\n\n@pytest.fixture(params=[\"simple_fastapi_app\", \"simple_fastapi_app_with_root_path\"])\ndef server(request: pytest.FixtureRequest) -> Generator[str, None, None]:\n    # Ensure COVERAGE_PROCESS_START is set in the environment for subprocesses\n    coverage_rc = os.path.abspath(\".coveragerc\")\n    os.environ[\"COVERAGE_PROCESS_START\"] = coverage_rc\n\n    # Get a free port\n    with socket.socket() as s:\n        s.bind((HOST, 0))\n        server_port = s.getsockname()[1]\n\n    # Use fork method to avoid pickling issues\n    ctx = multiprocessing.get_context(\"fork\")\n\n    # Run the server in a subprocess\n    fastapi_app = request.getfixturevalue(request.param)\n    proc = ctx.Process(\n        target=run_server,\n        kwargs={\"server_port\": server_port, \"fastapi_app\": fastapi_app},\n        daemon=True,\n    )\n    proc.start()\n\n    # Wait for server to be running\n    max_attempts = 20\n    attempt = 0\n    while attempt < max_attempts:\n        try:\n            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:\n                s.connect((HOST, server_port))\n                break\n        except ConnectionRefusedError:\n            time.sleep(0.1)\n            attempt += 1\n    else:\n        raise RuntimeError(f\"Server failed to start after {max_attempts} attempts\")\n\n    # Return the server URL\n    yield f\"http://{HOST}:{server_port}{fastapi_app.root_path}\"\n\n    # Signal the server to stop - added graceful shutdown before kill\n    try:\n        proc.terminate()\n        proc.join(timeout=2)\n    except (OSError, AttributeError):\n        pass\n\n    if proc.is_alive():\n        proc.kill()\n        proc.join(timeout=2)\n        if proc.is_alive():\n            raise RuntimeError(\"server process failed to terminate\")\n\n\n@pytest.fixture()\nasync def http_client(server: str) -> AsyncGenerator[httpx.AsyncClient, None]:\n    async with httpx.AsyncClient(base_url=server) as client:\n        yield client\n\n\n@pytest.mark.anyio\nasync def test_raw_sse_connection(http_client: httpx.AsyncClient, server: str) -> None:\n    \"\"\"Test the SSE connection establishment simply with an HTTP client.\"\"\"\n    from urllib.parse import urlparse\n\n    parsed_url = urlparse(server)\n    root_path = parsed_url.path\n    messages_path = f\"{root_path}/sse/messages/\" if root_path else \"/sse/messages/\"\n\n    async with anyio.create_task_group():\n\n        async def connection_test() -> None:\n            async with http_client.stream(\"GET\", \"/sse\") as response:\n                assert response.status_code == 200\n                assert response.headers[\"content-type\"] == \"text/event-stream; charset=utf-8\"\n\n                line_number = 0\n                async for line in response.aiter_lines():\n                    if line_number == 0:\n                        assert line == \"event: endpoint\"\n                    elif line_number == 1:\n                        assert line.startswith(f\"data: {messages_path}?session_id=\")\n                    else:\n                        return\n                    line_number += 1\n\n        # Add timeout to prevent test from hanging if it fails\n        with anyio.fail_after(3):\n            await connection_test()\n\n\n@pytest.mark.anyio\nasync def test_sse_basic_connection(server: str) -> None:\n    async with sse_client(server + \"/sse\") as streams:\n        async with ClientSession(*streams) as session:\n            # Test initialization\n            result = await session.initialize()\n            assert isinstance(result, InitializeResult)\n            assert result.serverInfo.name == SERVER_NAME\n\n            # Test ping\n            ping_result = await session.send_ping()\n            assert isinstance(ping_result, EmptyResult)\n\n\n@pytest.mark.anyio\nasync def test_sse_tool_call(server: str) -> None:\n    async with sse_client(server + \"/sse\") as streams:\n        async with ClientSession(*streams) as session:\n            await session.initialize()\n\n            tools_list_result = await session.list_tools()\n            assert isinstance(tools_list_result, ListToolsResult)\n            assert len(tools_list_result.tools) > 0\n\n            tool_call_result = await session.call_tool(\"get_item\", {\"item_id\": 1})\n            assert isinstance(tool_call_result, CallToolResult)\n            assert not tool_call_result.isError\n            assert tool_call_result.content is not None\n            assert len(tool_call_result.content) > 0\n"
  },
  {
    "path": "tests/test_types_validation.py",
    "content": "import pytest\nfrom pydantic import ValidationError\nfrom fastapi import Depends\n\nfrom fastapi_mcp.types import (\n    OAuthMetadata,\n    AuthConfig,\n)\n\n\nclass TestOAuthMetadata:\n    def test_non_empty_lists_validation(self):\n        for field in [\n            \"scopes_supported\",\n            \"response_types_supported\",\n            \"grant_types_supported\",\n            \"token_endpoint_auth_methods_supported\",\n            \"code_challenge_methods_supported\",\n        ]:\n            with pytest.raises(ValidationError, match=f\"{field} cannot be empty\"):\n                OAuthMetadata(\n                    issuer=\"https://example.com\",\n                    authorization_endpoint=\"https://example.com/auth\",\n                    token_endpoint=\"https://example.com/token\",\n                    **{field: []},\n                )\n\n    def test_authorization_endpoint_required_for_authorization_code(self):\n        with pytest.raises(ValidationError) as exc_info:\n            OAuthMetadata(\n                issuer=\"https://example.com\",\n                token_endpoint=\"https://example.com/token\",\n                grant_types_supported=[\"authorization_code\", \"client_credentials\"],\n            )\n        assert \"authorization_endpoint is required when authorization_code grant type is supported\" in str(\n            exc_info.value\n        )\n\n        OAuthMetadata(\n            issuer=\"https://example.com\",\n            token_endpoint=\"https://example.com/token\",\n            authorization_endpoint=\"https://example.com/auth\",\n            grant_types_supported=[\"client_credentials\"],\n        )\n\n    def test_model_dump_excludes_none(self):\n        metadata = OAuthMetadata(\n            issuer=\"https://example.com\",\n            authorization_endpoint=\"https://example.com/auth\",\n            token_endpoint=\"https://example.com/token\",\n        )\n\n        dumped = metadata.model_dump()\n\n        assert \"registration_endpoint\" not in dumped\n\n\nclass TestAuthConfig:\n    def test_required_fields_validation(self):\n        with pytest.raises(\n            ValidationError, match=\"at least one of 'issuer', 'custom_oauth_metadata' or 'dependencies' is required\"\n        ):\n            AuthConfig()\n\n        AuthConfig(issuer=\"https://example.com\")\n\n        AuthConfig(\n            custom_oauth_metadata={\n                \"issuer\": \"https://example.com\",\n                \"authorization_endpoint\": \"https://example.com/auth\",\n                \"token_endpoint\": \"https://example.com/token\",\n            },\n        )\n\n        def dummy_dependency():\n            pass\n\n        AuthConfig(dependencies=[Depends(dummy_dependency)])\n\n    def test_client_id_required_for_setup_proxies(self):\n        with pytest.raises(ValidationError, match=\"'client_id' is required when 'setup_proxies' is True\"):\n            AuthConfig(\n                issuer=\"https://example.com\",\n                setup_proxies=True,\n            )\n\n        AuthConfig(\n            issuer=\"https://example.com\",\n            setup_proxies=True,\n            client_id=\"test-client-id\",\n            client_secret=\"test-client-secret\",\n        )\n\n    def test_client_secret_required_for_fake_registration(self):\n        with pytest.raises(\n            ValidationError, match=\"'client_secret' is required when 'setup_fake_dynamic_registration' is True\"\n        ):\n            AuthConfig(\n                issuer=\"https://example.com\",\n                setup_proxies=True,\n                client_id=\"test-client-id\",\n                setup_fake_dynamic_registration=True,\n            )\n\n        AuthConfig(\n            issuer=\"https://example.com\",\n            setup_proxies=True,\n            client_id=\"test-client-id\",\n            client_secret=\"test-client-secret\",\n            setup_fake_dynamic_registration=True,\n        )\n"
  }
]