├── .cursor └── rules │ └── core-mcp-objects.mdc ├── .github ├── ISSUE_TEMPLATE │ ├── bug.yml │ ├── config.yml │ └── enhancement.yml ├── labeler.yml ├── release.yml └── workflows │ ├── labeler.yml │ ├── publish.yml │ ├── run-static.yml │ └── run-tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .python-version ├── AGENTS.md ├── LICENSE ├── README.md ├── Windows_Notes.md ├── docs ├── assets │ └── demo-inspector.png ├── clients │ ├── advanced-features.mdx │ ├── auth │ │ ├── bearer.mdx │ │ └── oauth.mdx │ ├── client.mdx │ └── transports.mdx ├── deployment │ ├── asgi.mdx │ ├── cli.mdx │ └── running-server.mdx ├── docs.json ├── getting-started │ ├── installation.mdx │ ├── quickstart.mdx │ └── welcome.mdx ├── patterns │ ├── contrib.mdx │ ├── decorating-methods.mdx │ ├── fastapi.mdx │ ├── http-requests.mdx │ └── testing.mdx ├── servers │ ├── auth │ │ └── bearer.mdx │ ├── composition.mdx │ ├── context.mdx │ ├── fastmcp.mdx │ ├── openapi.mdx │ ├── prompts.mdx │ ├── proxy.mdx │ ├── resources.mdx │ └── tools.mdx ├── snippets │ └── version-badge.mdx └── style.css ├── examples ├── complex_inputs.py ├── desktop.py ├── echo.py ├── in_memory_proxy_example.py ├── memory.py ├── mount_example.py ├── sampling.py ├── screenshot.py ├── serializer.py ├── simple_echo.py ├── smart_home │ ├── README.md │ ├── pyproject.toml │ ├── src │ │ └── smart_home │ │ │ ├── __init__.py │ │ │ ├── __main__.py │ │ │ ├── hub.py │ │ │ ├── lights │ │ │ ├── __init__.py │ │ │ ├── hue_utils.py │ │ │ └── server.py │ │ │ ├── py.typed │ │ │ └── settings.py │ └── uv.lock ├── tags_example.py └── text_me.py ├── justfile ├── pyproject.toml ├── src └── fastmcp │ ├── __init__.py │ ├── cli │ ├── __init__.py │ ├── claude.py │ ├── cli.py │ └── run.py │ ├── client │ ├── __init__.py │ ├── auth │ │ ├── __init__.py │ │ ├── bearer.py │ │ └── oauth.py │ ├── client.py │ ├── logging.py │ ├── oauth_callback.py │ ├── progress.py │ ├── roots.py │ ├── sampling.py │ └── transports.py │ ├── contrib │ ├── README.md │ ├── bulk_tool_caller │ │ ├── README.md │ │ ├── __init__.py │ │ ├── bulk_tool_caller.py │ │ └── example.py │ └── mcp_mixin │ │ ├── README.md │ │ ├── __init__.py │ │ ├── example.py │ │ └── mcp_mixin.py │ ├── exceptions.py │ ├── prompts │ ├── __init__.py │ ├── prompt.py │ └── prompt_manager.py │ ├── resources │ ├── __init__.py │ ├── resource.py │ ├── resource_manager.py │ ├── template.py │ └── types.py │ ├── server │ ├── __init__.py │ ├── auth │ │ ├── __init__.py │ │ ├── auth.py │ │ └── providers │ │ │ ├── __init__.py │ │ │ ├── bearer.py │ │ │ ├── bearer_env.py │ │ │ └── in_memory.py │ ├── context.py │ ├── dependencies.py │ ├── http.py │ ├── openapi.py │ ├── proxy.py │ └── server.py │ ├── settings.py │ ├── tools │ ├── __init__.py │ ├── tool.py │ └── tool_manager.py │ └── utilities │ ├── __init__.py │ ├── cache.py │ ├── decorators.py │ ├── exceptions.py │ ├── http.py │ ├── json_schema.py │ ├── logging.py │ ├── mcp_config.py │ ├── openapi.py │ ├── tests.py │ └── types.py ├── tests ├── __init__.py ├── auth │ ├── __init__.py │ ├── providers │ │ ├── test_bearer.py │ │ └── test_bearer_env.py │ └── test_oauth_client.py ├── cli │ ├── __init__.py │ ├── test_cli.py │ └── test_run.py ├── client │ ├── __init__.py │ ├── test_client.py │ ├── test_logs.py │ ├── test_openapi.py │ ├── test_progress.py │ ├── test_roots.py │ ├── test_sampling.py │ ├── test_sse.py │ ├── test_stdio.py │ └── test_streamable_http.py ├── conftest.py ├── contrib │ ├── __init__.py │ ├── test_bulk_tool_caller.py │ └── test_mcp_mixin.py ├── deprecated │ ├── __init__.py │ ├── test_deprecated.py │ ├── test_mount_separators.py │ ├── test_resource_prefixes.py │ └── test_route_type_ignore.py ├── prompts │ ├── __init__.py │ ├── test_prompt.py │ └── test_prompt_manager.py ├── resources │ ├── __init__.py │ ├── test_file_resources.py │ ├── test_function_resources.py │ ├── test_resource_manager.py │ ├── test_resource_template.py │ └── test_resources.py ├── server │ ├── __init__.py │ ├── http │ │ ├── __init__.py │ │ ├── test_custom_routes.py │ │ ├── test_http_dependencies.py │ │ └── test_http_middleware.py │ ├── openapi │ │ ├── __init__.py │ │ ├── test_openapi.py │ │ ├── test_openapi_path_parameters.py │ │ └── test_route_map_fn.py │ ├── test_app_state.py │ ├── test_auth_integration.py │ ├── test_context.py │ ├── test_file_server.py │ ├── test_import_server.py │ ├── test_logging.py │ ├── test_mount.py │ ├── test_proxy.py │ ├── test_resource_prefix_formats.py │ ├── test_run_server.py │ ├── test_server.py │ ├── test_server_interactions.py │ ├── test_tool_annotations.py │ └── test_tool_exclude_args.py ├── test_examples.py ├── test_servers │ ├── fastmcp_server.py │ ├── sse.py │ └── stdio.py ├── tools │ ├── __init__.py │ ├── test_tool.py │ └── test_tool_manager.py └── utilities │ ├── __init__.py │ ├── openapi │ ├── __init__.py │ ├── conftest.py │ ├── test_openapi.py │ ├── test_openapi_advanced.py │ └── test_openapi_fastapi.py │ ├── test_cache.py │ ├── test_decorated_function.py │ ├── test_json_schema.py │ ├── test_logging.py │ ├── test_mcp_config.py │ ├── test_tests.py │ ├── test_typeadapter.py │ └── test_types.py └── uv.lock /.cursor/rules/core-mcp-objects.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: true 5 | --- 6 | There are four major MCP object types: 7 | 8 | - Tools (src/tools/) 9 | - Resources (src/resources/) 10 | - Resource Templates (src/resources/) 11 | - Prompts (src/prompts) 12 | 13 | While these have slightly different semantics and implementations, in general changes that affect interactions with any one (like adding tags, importing, etc.) will need to be adopted, applied, and tested on all others. Be sure to look at not only the object definition but also the related `Manager` (e.g. `ToolManager`, `ResourceManager`, and `PromptManager`). Also note that while resources and resource templates are different objects, they both are handled by the `ResourceManager`. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug Report 2 | description: Report a bug or unexpected behavior in FastMCP 3 | labels: [bug, pending] 4 | 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: Thank you for contributing to FastMCP! 🙏 9 | 10 | - type: textarea 11 | id: description 12 | attributes: 13 | label: Description 14 | description: | 15 | Please explain what you're experiencing and what you would expect to happen instead. 16 | 17 | Provide as much detail as possible to help us understand and solve your problem quickly. 18 | validations: 19 | required: true 20 | 21 | - type: textarea 22 | id: example 23 | attributes: 24 | label: Example Code 25 | description: > 26 | If applicable, please provide a self-contained, 27 | [minimal, reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) 28 | demonstrating the bug. 29 | 30 | If possible, your example should be a single-file script. Instead of `.run()`-ing an MCP server, use a `Client` to directly interact with it (`async with Client(mcp) as client: ...`) and demonstrate the issue. 31 | 32 | placeholder: | 33 | from fastmcp import FastMCP, Client 34 | 35 | mcp = FastMCP() 36 | 37 | async with Client(mcp) as client: 38 | ... 39 | render: Python 40 | 41 | - type: textarea 42 | id: version 43 | attributes: 44 | label: Version Information 45 | description: | 46 | Please provide information about your FastMCP version, MCP version, Python version, and OS. 47 | 48 | To get this information, run the following command in your terminal and paste the output below: 49 | 50 | ```bash 51 | fastmcp version 52 | ``` 53 | 54 | If there is other information that would be helpful, please include it as well. 55 | render: Text 56 | validations: 57 | required: true 58 | 59 | - type: textarea 60 | id: additional_context 61 | attributes: 62 | label: Additional Context 63 | description: | 64 | Add any other context about the problem here. This could include: 65 | - The full error message and traceback (if applicable) 66 | - Information about your environment (e.g., virtual environment, installed packages) 67 | - Steps to reproduce the issue 68 | - Any recent changes in your code or setup that might be relevant 69 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: FastMCP Documentation 4 | url: https://gofastmcp.com 5 | about: Please review the documentation before opening an issue. 6 | - name: MCP Python SDK 7 | url: https://github.com/modelcontextprotocol/python-sdk/issues 8 | about: Issues related to the low-level MCP Python SDK, including the FastMCP 1.0 module that is included in the `mcp` package, should be filed on the official MCP repository. 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/enhancement.yml: -------------------------------------------------------------------------------- 1 | name: 💡 Enhancement Request 2 | description: Suggest an idea or improvement for FastMCP 3 | labels: [enhancement, pending] 4 | 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: Thank you for contributing to FastMCP! We value your ideas for improving the framework. 💡 9 | 10 | - type: textarea 11 | id: description 12 | attributes: 13 | label: Enhancement Description 14 | description: | 15 | Please describe the enhancement you'd like to see in FastMCP. 16 | 17 | - What problem would this solve? 18 | - How would this improve your workflow or experience with FastMCP? 19 | - Are there any alternative solutions you've considered? 20 | validations: 21 | required: true 22 | 23 | - type: textarea 24 | id: use_case 25 | attributes: 26 | label: Use Case 27 | description: | 28 | Describe a specific use case or scenario where this enhancement would be beneficial. 29 | If possible, provide an example of how you envision using this feature. 30 | 31 | - type: textarea 32 | id: example 33 | attributes: 34 | label: Proposed Implementation 35 | description: > 36 | If you have ideas about how this enhancement could be implemented, 37 | please share them here. Code snippets, pseudocode, or general approaches are welcome. 38 | render: Python 39 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | documentation: 2 | - changed-files: 3 | - any-glob-to-any-file: "docs/**" 4 | 5 | example: 6 | - changed-files: 7 | - any-glob-to-any-file: 8 | - "examples/**" 9 | 10 | tests: 11 | - changed-files: 12 | - any-glob-to-any-file: "tests/**" 13 | 14 | "component: HTTP": 15 | - changed-files: 16 | - any-glob-to-any-file: "src/fastmcp/server/http.py" 17 | 18 | "component: client": 19 | - changed-files: 20 | - any-glob-to-any-file: "src/fastmcp/client/**" 21 | 22 | "contrib": 23 | - changed-files: 24 | - any-glob-to-any-file: "src/fastmcp/contrib/**" 25 | 26 | "component: openapi": 27 | - changed-files: 28 | - any-glob-to-any-file: 29 | - "src/**/*openapi*.py" 30 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ignore in release notes 5 | 6 | categories: 7 | - title: New Features 🎉 8 | labels: 9 | - feature 10 | - enhancement 11 | exclude: 12 | labels: 13 | - breaking change 14 | 15 | - title: Fixes 🐞 16 | labels: 17 | - bug 18 | exclude: 19 | labels: 20 | - breaking change 21 | 22 | - title: Breaking Changes 🛫 23 | labels: 24 | - breaking change 25 | 26 | - title: Docs 📚 27 | labels: 28 | - documentation 29 | 30 | - title: Other Changes 🦾 31 | labels: 32 | - "*" 33 | -------------------------------------------------------------------------------- /.github/workflows/labeler.yml: -------------------------------------------------------------------------------- 1 | name: "Pull Request Labeler" 2 | on: 3 | - pull_request_target 4 | 5 | jobs: 6 | labeler: 7 | permissions: 8 | contents: read 9 | pull-requests: write 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/labeler@v5 13 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish FastMCP to PyPI 2 | on: 3 | release: 4 | types: [published] 5 | workflow_dispatch: 6 | 7 | jobs: 8 | pypi-publish: 9 | name: Upload to PyPI 10 | runs-on: ubuntu-latest 11 | permissions: 12 | id-token: write # For PyPI's trusted publishing 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: "Install uv" 20 | uses: astral-sh/setup-uv@v3 21 | 22 | - name: Build 23 | run: uv build 24 | 25 | - name: Publish to PyPi 26 | run: uv publish -v dist/* 27 | -------------------------------------------------------------------------------- /.github/workflows/run-static.yml: -------------------------------------------------------------------------------- 1 | name: Run static analysis 2 | 3 | env: 4 | # enable colored output 5 | # https://github.com/pytest-dev/pytest/issues/7443 6 | PY_COLORS: 1 7 | 8 | on: 9 | push: 10 | branches: ["main"] 11 | paths: 12 | - "src/**" 13 | - "tests/**" 14 | - "uv.lock" 15 | - "pyproject.toml" 16 | - ".github/workflows/**" 17 | 18 | # run on all pull requests because these checks are required and will block merges otherwise 19 | pull_request: 20 | 21 | workflow_dispatch: 22 | 23 | permissions: 24 | contents: read 25 | 26 | jobs: 27 | static_analysis: 28 | timeout-minutes: 2 29 | 30 | runs-on: ubuntu-latest 31 | 32 | steps: 33 | - uses: actions/checkout@v4 34 | - name: Install uv 35 | uses: astral-sh/setup-uv@v5 36 | with: 37 | enable-cache: true 38 | cache-dependency-glob: "uv.lock" 39 | - name: Set up Python 40 | uses: actions/setup-python@v5 41 | with: 42 | python-version: "3.12" 43 | - name: Install dependencies 44 | run: uv sync --dev 45 | - name: Run pre-commit 46 | uses: pre-commit/action@v3.0.1 47 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | env: 4 | # enable colored output 5 | PY_COLORS: 1 6 | 7 | on: 8 | push: 9 | branches: ["main"] 10 | paths: 11 | - "src/**" 12 | - "tests/**" 13 | - "uv.lock" 14 | - "pyproject.toml" 15 | - ".github/workflows/**" 16 | 17 | # run on all pull requests because these checks are required and will block merges otherwise 18 | pull_request: 19 | 20 | workflow_dispatch: 21 | 22 | permissions: 23 | contents: read 24 | 25 | jobs: 26 | run_tests: 27 | name: "Run tests: Python ${{ matrix.python-version }} on ${{ matrix.os }}" 28 | runs-on: ${{ matrix.os }} 29 | strategy: 30 | matrix: 31 | os: [ubuntu-latest, windows-latest] 32 | python-version: ["3.10"] 33 | fail-fast: false 34 | timeout-minutes: 5 35 | 36 | steps: 37 | - uses: actions/checkout@v4 38 | 39 | - name: Install uv 40 | uses: astral-sh/setup-uv@v5 41 | with: 42 | enable-cache: true 43 | cache-dependency-glob: "uv.lock" 44 | python-version: ${{ matrix.python-version }} 45 | 46 | - name: Install FastMCP 47 | run: uv sync --locked 48 | 49 | - name: Run tests 50 | run: uv run pytest tests 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python-generated files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | build/ 6 | dist/ 7 | wheels/ 8 | *.egg-info/ 9 | *.egg 10 | MANIFEST 11 | .pytest_cache/ 12 | .coverage 13 | htmlcov/ 14 | .tox/ 15 | nosetests.xml 16 | coverage.xml 17 | *.cover 18 | 19 | # Virtual environments 20 | .venv 21 | venv/ 22 | env/ 23 | ENV/ 24 | .env 25 | 26 | # System files 27 | .DS_Store 28 | 29 | # Version file 30 | src/fastmcp/_version.py 31 | 32 | # Editors and IDEs 33 | .cursorrules 34 | .vscode/ 35 | .idea/ 36 | *.swp 37 | *.swo 38 | *~ 39 | .project 40 | .pydevproject 41 | .settings/ 42 | 43 | # Jupyter Notebook 44 | .ipynb_checkpoints 45 | 46 | # Type checking 47 | .mypy_cache/ 48 | .dmypy.json 49 | dmypy.json 50 | .pyre/ 51 | .pytype/ 52 | 53 | # Local development 54 | .python-version 55 | .envrc 56 | .direnv/ 57 | 58 | # Logs and databases 59 | *.log 60 | *.sqlite 61 | *.db 62 | *.ddb 63 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | fail_fast: true 2 | 3 | repos: 4 | - repo: https://github.com/abravalheri/validate-pyproject 5 | rev: v0.23 6 | hooks: 7 | - id: validate-pyproject 8 | 9 | - repo: https://github.com/pre-commit/mirrors-prettier 10 | rev: v3.1.0 11 | hooks: 12 | - id: prettier 13 | types_or: [yaml, json5] 14 | 15 | - repo: https://github.com/astral-sh/ruff-pre-commit 16 | # Ruff version. 17 | rev: v0.11.4 18 | hooks: 19 | # Run the linter. 20 | - id: ruff 21 | args: [--fix, --exit-non-zero-on-fix] 22 | # Run the formatter. 23 | - id: ruff-format 24 | 25 | - repo: https://github.com/northisup/pyright-pretty 26 | rev: v0.1.0 27 | hooks: 28 | - id: pyright-pretty 29 | files: ^src/|^tests/ 30 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.12 2 | -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | # AGENTS 2 | 3 | > **Audience**: LLM-driven engineering agents 4 | 5 | This file provides guidance for autonomous coding agents working inside the **FastMCP** repository. 6 | 7 | --- 8 | 9 | ## Repository map 10 | 11 | | Path | Purpose | 12 | | ---------------- | ---------------------------------------------------------------------------------------- | 13 | | `src/fastmcp/` | Library source code (Python ≥ 3.10) | 14 | | ` └─server/` | Server implementation, `FastMCP`, auth, networking | 15 | | ` └─client/` | High‑level client SDK + helpers | 16 | | ` └─resources/` | MCP resources and resource templates | 17 | | ` └─prompts/` | Prompt templates | 18 | | ` └─tools/` | Tool implementations | 19 | | `tests/` | Pytest test‑suite | 20 | | `docs/` | Mintlify‑flavoured Markdown, published to [https://gofastmcp.com](https://gofastmcp.com) | 21 | | `examples/` | Minimal runnable demos | 22 | 23 | --- 24 | 25 | ## Mandatory dev workflow 26 | 27 | ```bash 28 | uv sync # install dependencies 29 | uv run pre-commit run --all-files # Ruff + Prettier + Pyright 30 | uv run pytest # run full test suite 31 | ``` 32 | 33 | *Tests must pass* and *lint/typing must be clean* before committing. 34 | 35 | ### Core MCP objects 36 | 37 | There are four major MCP object types: 38 | 39 | - Tools (`src/tools/`) 40 | - Resources (`src/resources/`) 41 | - Resource Templates (`src/resources/`) 42 | - Prompts (`src/prompts`) 43 | 44 | While these have slightly different semantics and implementations, in general changes that affect interactions with any one (like adding tags, importing, etc.) will need to be adopted, applied, and tested on all others. Be sure to look at not only the object definition but also the related `Manager` (e.g. `ToolManager`, `ResourceManager`, and `PromptManager`). Also note that while resources and resource templates are different objects, they both are handled by the `ResourceManager`. 45 | 46 | --- 47 | 48 | ## Code conventions 49 | 50 | * **Language:** Python ≥ 3.10 51 | * **Style:** Enforced through pre-commit hooks 52 | * **Type-checking:** Fully typed codebase 53 | * **Tests:** Each feature should have corresponding tests 54 | 55 | --- 56 | 57 | ## Development guidelines 58 | 59 | 1. **Set up** the environment: 60 | ```bash 61 | uv sync && uv run pre-commit run --all-files 62 | ``` 63 | 2. **Run tests**: `uv run pytest` until they pass. 64 | 3. **Iterate**: if a command fails, read the output, fix the code, retry. 65 | 4. Make the smallest set of changes that achieve the desired outcome. 66 | 5. Always read code before modifying it blindly. 67 | 6. Follow established patterns and maintain consistency. 68 | -------------------------------------------------------------------------------- /Windows_Notes.md: -------------------------------------------------------------------------------- 1 | # Getting your development environment set up properly 2 | To get your environment up and running properly, you'll need a slightly different set of commands that are windows specific: 3 | ```bash 4 | uv venv 5 | .venv\Scripts\activate 6 | uv pip install -e ".[dev]" 7 | ``` 8 | 9 | This will install the package in editable mode, and install the development dependencies. 10 | 11 | 12 | # Fixing `AttributeError: module 'collections' has no attribute 'Callable'` 13 | - open `.venv\Lib\site-packages\pyreadline\py3k_compat.py` 14 | - change `return isinstance(x, collections.Callable)` to 15 | ``` 16 | from collections.abc import Callable 17 | return isinstance(x, Callable) 18 | ``` 19 | 20 | # Helpful notes 21 | For developing FastMCP 22 | ## Install local development version of FastMCP into a local FastMCP project server 23 | - ensure 24 | - change directories to your FastMCP Server location so you can install it in your .venv 25 | - run `.venv\Scripts\activate` to activate your virtual environment 26 | - Then run a series of commands to uninstall the old version and install the new 27 | ```bash 28 | # First uninstall 29 | uv pip uninstall fastmcp 30 | 31 | # Clean any build artifacts in your fastmcp directory 32 | cd C:\path\to\fastmcp 33 | del /s /q *.egg-info 34 | 35 | # Then reinstall in your weather project 36 | cd C:\path\to\new\fastmcp_server 37 | uv pip install --no-cache-dir -e C:\Users\justj\PycharmProjects\fastmcp 38 | 39 | # Check that it installed properly and has the correct git hash 40 | pip show fastmcp 41 | ``` 42 | 43 | ## Running the FastMCP server with Inspector 44 | MCP comes with a node.js application called Inspector that can be used to inspect the FastMCP server. To run the inspector, you'll need to install node.js and npm. Then you can run the following commands: 45 | ```bash 46 | fastmcp dev server.py 47 | ``` 48 | This will launch a web app on http://localhost:5173/ that you can use to inspect the FastMCP server. 49 | 50 | ## If you start development before creating a fork - your get out of jail free card 51 | - Add your fork as a new remote to your local repository `git remote add fork git@github.com:YOUR-USERNAME/REPOSITORY-NAME.git` 52 | - This will add your repo, short named 'fork', as a remote to your local repository 53 | - Verify that it was added correctly by running `git remote -v` 54 | - Commit your changes 55 | - Push your changes to your fork `git push fork ` 56 | - Create your pull request on GitHub 57 | 58 | 59 | -------------------------------------------------------------------------------- /docs/assets/demo-inspector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlowin/fastmcp/97ef80c62478d0f366d62baf3e15875937cd7b9f/docs/assets/demo-inspector.png -------------------------------------------------------------------------------- /docs/clients/auth/bearer.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Bearer Token Authentication 3 | sidebarTitle: Bearer Auth 4 | description: Authenticate your FastMCP client using pre-existing OAuth 2.0 Bearer tokens. 5 | icon: key 6 | --- 7 | 8 | import { VersionBadge } from "/snippets/version-badge.mdx" 9 | 10 | 11 | 12 | 13 | Bearer Token authentication is only relevant for HTTP-based transports. 14 | 15 | 16 | You can configure your FastMCP client to use **bearer authentication** by supplying a valid access token. This is most appropriate for service accounts, long-lived API keys, CI/CD, applications where authentication is managed separately, or other non-interactive authentication methods. 17 | 18 | A Bearer token is a JSON Web Token (JWT) that is used to authenticate a request. It is most commonly used in the `Authorization` header of an HTTP request, using the `Bearer` scheme: 19 | 20 | ```http 21 | Authorization: Bearer 22 | ``` 23 | 24 | 25 | ## Client Usage 26 | 27 | The most straightforward way to use a pre-existing Bearer token is to provide it as a string to the `auth` parameter of the `fastmcp.Client` or transport instance. FastMCP will automatically format it correctly for the `Authorization` header and bearer scheme. 28 | 29 | 30 | If you're using a string token, do not include the `Bearer` prefix. FastMCP will add it for you. 31 | 32 | 33 | ```python {4} 34 | from fastmcp import Client 35 | 36 | async with Client( 37 | "https://fastmcp.cloud/mcp", auth="" 38 | ) as client: 39 | await client.ping() 40 | ``` 41 | 42 | You can also supply a Bearer token to a transport instance, such as `StreamableHttpTransport` or `SSETransport`: 43 | 44 | ```python {5} 45 | from fastmcp import Client 46 | from fastmcp.client.transports import StreamableHttpTransport 47 | 48 | transport = StreamableHttpTransport( 49 | "http://fastmcp.cloud/mcp", auth="" 50 | ) 51 | 52 | async with Client(transport) as client: 53 | await client.ping() 54 | ``` 55 | 56 | ## `BearerAuth` Helper 57 | 58 | If you prefer to be more explicit and not rely on FastMCP to transform your string token, you can use the `BearerAuth` class yourself, which implements the `httpx.Auth` interface. 59 | 60 | ```python {5} 61 | from fastmcp import Client 62 | from fastmcp.client.auth import BearerAuth 63 | 64 | async with Client( 65 | "https://fastmcp.cloud/mcp", auth=BearerAuth(token="") 66 | ) as client: 67 | await client.ping() 68 | ``` 69 | 70 | ## Custom Headers 71 | 72 | If the MCP server expects a custom header or token scheme, you can manually set the client's `headers` instead of using the `auth` parameter: 73 | 74 | ```python {4} 75 | from fastmcp import Client 76 | 77 | async with Client( 78 | "https://fastmcp.cloud/mcp", headers={"X-API-Key": ""} 79 | ) as client: 80 | await client.ping() 81 | ``` 82 | -------------------------------------------------------------------------------- /docs/docs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://mintlify.com/docs.json", 3 | "background": { 4 | "color": { 5 | "dark": "#222831", 6 | "light": "#EEEEEE" 7 | }, 8 | "decoration": "windows" 9 | }, 10 | "colors": { 11 | "dark": "#f72585", 12 | "light": "#4cc9f0", 13 | "primary": "#2d00f7" 14 | }, 15 | "description": "The fast, Pythonic way to build MCP servers and clients.", 16 | "footer": { 17 | "socials": { 18 | "bluesky": "https://bsky.app/profile/jlowin.dev", 19 | "github": "https://github.com/jlowin/fastmcp", 20 | "x": "https://x.com/jlowin" 21 | } 22 | }, 23 | "integrations": { 24 | "ga4": { 25 | "measurementId": "G-64R5W1TJXG" 26 | } 27 | }, 28 | "name": "FastMCP", 29 | "navbar": { 30 | "primary": { 31 | "href": "https://github.com/jlowin/fastmcp", 32 | "type": "github" 33 | } 34 | }, 35 | "navigation": { 36 | "groups": [ 37 | { 38 | "group": "Get Started", 39 | "pages": [ 40 | "getting-started/welcome", 41 | "getting-started/installation", 42 | "getting-started/quickstart" 43 | ] 44 | }, 45 | { 46 | "group": "Servers", 47 | "pages": [ 48 | "servers/fastmcp", 49 | { 50 | "group": "Core Components", 51 | "icon": "toolbox", 52 | "pages": [ 53 | "servers/tools", 54 | "servers/resources", 55 | "servers/prompts", 56 | "servers/context" 57 | ] 58 | }, 59 | { 60 | "group": "Authentication", 61 | "icon": "shield-check", 62 | "pages": [ 63 | "servers/auth/bearer" 64 | ] 65 | }, 66 | "servers/openapi", 67 | "servers/proxy", 68 | "servers/composition", 69 | { 70 | "group": "Deployment", 71 | "pages": [ 72 | "deployment/running-server", 73 | "deployment/asgi", 74 | "deployment/cli" 75 | ] 76 | } 77 | ] 78 | }, 79 | { 80 | "group": "Clients", 81 | "pages": [ 82 | "clients/client", 83 | "clients/transports", 84 | { 85 | "group": "Authentication", 86 | "icon": "user-shield", 87 | "pages": [ 88 | "clients/auth/bearer", 89 | "clients/auth/oauth" 90 | ] 91 | }, 92 | "clients/advanced-features" 93 | ] 94 | }, 95 | { 96 | "group": "Patterns", 97 | "pages": [ 98 | "patterns/decorating-methods", 99 | "patterns/http-requests", 100 | "patterns/contrib", 101 | "patterns/testing" 102 | ] 103 | }, 104 | { 105 | "group": "Deployment", 106 | "pages": [] 107 | } 108 | ] 109 | }, 110 | "redirects": [ 111 | { 112 | "destination": "/servers/proxy", 113 | "source": "/patterns/proxy" 114 | }, 115 | { 116 | "destination": "/servers/composition", 117 | "source": "/patterns/composition" 118 | } 119 | ], 120 | "theme": "mint" 121 | } -------------------------------------------------------------------------------- /docs/getting-started/installation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | icon: arrow-down-to-line 4 | --- 5 | ## Install FastMCP 6 | 7 | We recommend using [uv](https://docs.astral.sh/uv/getting-started/installation/) to install and manage FastMCP. 8 | 9 | If you plan to use FastMCP in your project, you can add it as a dependency with: 10 | 11 | ```bash 12 | uv add fastmcp 13 | ``` 14 | 15 | Alternatively, you can install it directly with `pip` or `uv pip`: 16 | 17 | ```bash uv 18 | uv pip install fastmcp 19 | ``` 20 | 21 | ```bash pip 22 | pip install fastmcp 23 | ``` 24 | 25 | 26 | ### Verify Installation 27 | 28 | To verify that FastMCP is installed correctly, you can run the following command: 29 | 30 | ```bash 31 | fastmcp version 32 | ``` 33 | 34 | You should see output like the following: 35 | 36 | ```bash 37 | $ fastmcp version 38 | 39 | FastMCP version: 0.4.2.dev41+ga077727.d20250410 40 | MCP version: 1.6.0 41 | Python version: 3.12.2 42 | Platform: macOS-15.3.1-arm64-arm-64bit 43 | FastMCP root path: ~/Developer/fastmcp 44 | ``` 45 | ## Upgrading from the Official MCP SDK 46 | 47 | Upgrading from the official MCP SDK's FastMCP 1.0 to FastMCP 2.0 is generally straightforward. The core server API is highly compatible, and in many cases, changing your import statement from `from mcp.server.fastmcp import FastMCP` to `from fastmcp import FastMCP` will be sufficient. 48 | 49 | 50 | ```python {1-5} 51 | # Before 52 | # from mcp.server.fastmcp import FastMCP 53 | 54 | # After 55 | from fastmcp import FastMCP 56 | 57 | mcp = FastMCP("My MCP Server") 58 | ``` 59 | 60 | Prior to `fastmcp==2.3.0` and `mcp==1.8.0`, the 2.x API always mirrored the 1.0 API. However, as the projects diverge, this can not be guaranteed. You may see deprecation warnings if you attempt to use 1.0 APIs in FastMCP 2.x. Please refer to this documentation for details on new capabilities. 61 | 62 | 63 | ## Versioning and Breaking Changes 64 | 65 | While we make every effort not to introduce backwards-incompatible changes to our public APIs and behavior, FastMCP exists in a rapidly evolving MCP landscape. We're committed to bringing the most cutting-edge features to our users, which occasionally necessitates changes to existing functionality. 66 | 67 | As a practice, breaking changes will only occur on minor version changes (e.g., 2.3.x to 2.4.0). A minor version change indicates either: 68 | - A significant new feature set that warrants a new minor version 69 | - Introducing breaking changes that may affect behavior on upgrade 70 | 71 | For users concerned about stability in production environments, we recommend pinning FastMCP to a specific version in your dependencies. 72 | 73 | Whenever possible, FastMCP will issue deprecation warnings when users attempt to use APIs that are either deprecated or destined for future removal. These warnings will be maintained for at least 1 minor version release, and may be maintained longer. 74 | 75 | Note that the "public API" includes the core functionality of the `FastMCP` server and its methods. It does not include private methods or objects that are stored as private attributes, as we do not expect users to rely on those implementation details. 76 | 77 | ## Installing for Development 78 | 79 | If you plan to contribute to FastMCP, you should begin by cloning the repository and using uv to install all dependencies (development dependencies are installed automatically): 80 | 81 | ```bash 82 | git clone https://github.com/jlowin/fastmcp.git 83 | cd fastmcp 84 | uv sync 85 | ``` 86 | 87 | This will install all dependencies, including ones for development, and create a virtual environment, which you can activate and use as normal. 88 | 89 | ### Unit Tests 90 | 91 | FastMCP has a comprehensive unit test suite, and all PR's must introduce and pass appropriate tests. To run the tests, use pytest: 92 | 93 | ```bash 94 | pytest 95 | ``` 96 | 97 | ### Pre-Commit Hooks 98 | 99 | FastMCP uses pre-commit to manage code quality, including formatting, linting, and type-safety. All PRs must pass the pre-commit hooks, which are run as a part of the CI process. To install the pre-commit hooks, run: 100 | 101 | ```bash 102 | uv run pre-commit install 103 | ``` 104 | 105 | Alternatively, to run pre-commit manually at any time, use: 106 | 107 | ```bash 108 | pre-commit run --all-files 109 | ``` 110 | -------------------------------------------------------------------------------- /docs/getting-started/quickstart.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Quickstart 3 | icon: rocket-launch 4 | --- 5 | 6 | Welcome! This guide will help you quickly set up FastMCP and run your first MCP server. 7 | 8 | If you haven't already installed FastMCP, follow the [installation instructions](/getting-started/installation). 9 | 10 | ## Creating a FastMCP Server 11 | 12 | A FastMCP server is a collection of tools, resources, and other MCP components. To create a server, start by instantiating the `FastMCP` class. 13 | 14 | Create a new file called `my_server.py` and add the following code: 15 | 16 | ```python my_server.py 17 | from fastmcp import FastMCP 18 | 19 | mcp = FastMCP("My MCP Server") 20 | ``` 21 | 22 | 23 | That's it! You've created a FastMCP server, albeit a very boring one. Let's add a tool to make it more interesting. 24 | 25 | 26 | ## Adding a Tool 27 | 28 | To add a tool that returns a simple greeting, write a function and decorate it with `@mcp.tool` to register it with the server: 29 | 30 | ```python my_server.py {5-7} 31 | from fastmcp import FastMCP 32 | 33 | mcp = FastMCP("My MCP Server") 34 | 35 | @mcp.tool() 36 | def greet(name: str) -> str: 37 | return f"Hello, {name}!" 38 | ``` 39 | 40 | 41 | ## Testing the Server 42 | 43 | 44 | To test the server, create a FastMCP client and point it at the server object. 45 | 46 | ```python my_server.py {1-2, 10-17} 47 | import asyncio 48 | from fastmcp import FastMCP, Client 49 | 50 | mcp = FastMCP("My MCP Server") 51 | 52 | @mcp.tool() 53 | def greet(name: str) -> str: 54 | return f"Hello, {name}!" 55 | 56 | client = Client(mcp) 57 | 58 | async def call_tool(name: str): 59 | async with client: 60 | result = await client.call_tool("greet", {"name": name}) 61 | print(result) 62 | 63 | asyncio.run(call_tool("Ford")) 64 | ``` 65 | 66 | There are a few things to note here: 67 | - Clients are asynchronous, so we need to use `asyncio.run` to run the client. 68 | - We must enter a client context (`async with client:`) before using the client. You can make multiple client calls within the same context. 69 | 70 | ## Running the server 71 | 72 | In order to run the server with Python, we need to add a `run` statement to the `__main__` block of the server file. 73 | 74 | ```python my_server.py {9-10} 75 | from fastmcp import FastMCP 76 | 77 | mcp = FastMCP("My MCP Server") 78 | 79 | @mcp.tool() 80 | def greet(name: str) -> str: 81 | return f"Hello, {name}!" 82 | 83 | if __name__ == "__main__": 84 | mcp.run() 85 | ``` 86 | 87 | This lets us run the server with `python my_server.py`, using the default `stdio` transport, which is the standard way to expose an MCP server to a client. 88 | 89 | 90 | Why do we need the `if __name__ == "__main__":` block? 91 | 92 | Within the FastMCP ecosystem, this line may be unnecessary. However, including it ensures that your FastMCP server runs for all users and clients in a consistent way and is therefore recommended as best practice. 93 | 94 | 95 | ### Interacting with the Python server 96 | 97 | Now that the server can be executed with `python my_server.py`, we can interact with it like any other MCP server. 98 | 99 | In a new file, create a client and point it at the server file: 100 | 101 | ```python my_client.py 102 | import asyncio 103 | from fastmcp import Client 104 | 105 | client = Client("my_server.py") 106 | 107 | async def call_tool(name: str): 108 | async with client: 109 | result = await client.call_tool("greet", {"name": name}) 110 | print(result) 111 | 112 | asyncio.run(call_tool("Ford")) 113 | ``` 114 | 115 | 116 | 117 | ### Using the FastMCP CLI 118 | 119 | To have FastMCP run the server for us, we can use the `fastmcp run` command. This will start the server and keep it running until it is stopped. By default, it will use the `stdio` transport, which is a simple text-based protocol for interacting with the server. 120 | 121 | ```bash 122 | fastmcp run my_server.py:mcp 123 | ``` 124 | 125 | Note that FastMCP *does not* require the `__main__` block in the server file, and will ignore it if it is present. Instead, it looks for the server object provided in the CLI command (here, `mcp`). If no server object is provided, `fastmcp run` will automatically search for servers called "mcp", "app", or "server" in the file. 126 | 127 | 128 | We pointed our client at the server file, which is recognized as a Python MCP server and executed with `python my_server.py` by default. This executes the `__main__` block of the server file. There are other ways to run the server, which are described in the [server configuration](/servers/fastmcp#running-the-server) guide. 129 | 130 | -------------------------------------------------------------------------------- /docs/getting-started/welcome.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Welcome to FastMCP 2.0!" 3 | sidebarTitle: "Welcome!" 4 | description: The fast, Pythonic way to build MCP servers and clients. 5 | 6 | icon: hand-wave 7 | --- 8 | 9 | 10 | The [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) is a new, standardized way to provide context and tools to your LLMs, and FastMCP makes building MCP servers and clients simple and intuitive. Create tools, expose resources, define prompts, and more with clean, Pythonic code: 11 | 12 | ```python {1, 3, 5, 11} 13 | from fastmcp import FastMCP 14 | 15 | mcp = FastMCP("Demo 🚀") 16 | 17 | @mcp.tool() 18 | def add(a: int, b: int) -> int: 19 | """Add two numbers""" 20 | return a + b 21 | 22 | if __name__ == "__main__": 23 | mcp.run() 24 | ``` 25 | 26 | 27 | ## FastMCP and the Official MCP SDK 28 | 29 | FastMCP is the standard framework for building MCP servers and clients. FastMCP 1.0 was incorporated into the [official MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk). 30 | 31 | **This is FastMCP 2.0,** the [actively maintained version](https://github.com/jlowin/fastmcp) that significantly expands on 1.0's basic server-building capabilities by introducing full client support, server composition, OpenAPI/FastAPI integration, remote server proxying, built-in testing tools, and more. 32 | 33 | FastMCP 2.0 is the complete toolkit for modern AI applications. Ready to upgrade or get started? Follow the [installation instructions](/getting-started/installation), which include specific steps for upgrading from the official MCP SDK. 34 | 35 | 36 | ## What is MCP? 37 | The Model Context Protocol lets you build servers that expose data and functionality to LLM applications in a secure, standardized way. It is often described as "the USB-C port for AI", providing a uniform way to connect LLMs to resources they can use. It may be easier to think of it as an API, but specifically designed for LLM interactions. MCP servers can: 38 | 39 | - Expose data through `Resources` (think of these sort of like GET endpoints; they are used to load information into the LLM's context) 40 | - Provide functionality through `Tools` (sort of like POST endpoints; they are used to execute code or otherwise produce a side effect) 41 | - Define interaction patterns through `Prompts` (reusable templates for LLM interactions) 42 | - And more! 43 | 44 | There is a low-level Python SDK available for implementing the protocol directly, but FastMCP aims to make that easier by providing a high-level, Pythonic interface. 45 | 46 | 47 | ## Why FastMCP? 48 | 49 | The MCP protocol is powerful but implementing it involves a lot of boilerplate - server setup, protocol handlers, content types, error management. FastMCP handles all the complex protocol details and server management, so you can focus on building great tools. It's designed to be high-level and Pythonic; in most cases, decorating a function is all you need. 50 | 51 | While the core server concepts of FastMCP 1.0 laid the groundwork and were contributed to the official MCP SDK, FastMCP 2.0 (this project) is the actively developed successor, adding significant enhancements and entirely new capabilities like a powerful client library, server proxying, composition patterns, and much more. 52 | 53 | FastMCP aims to be: 54 | 55 | 🚀 **Fast**: High-level interface means less code and faster development 56 | 57 | 🍀 **Simple**: Build MCP servers with minimal boilerplate 58 | 59 | 🐍 **Pythonic**: Feels natural to Python developers 60 | 61 | 🔍 **Complete**: FastMCP aims to provide a full implementation of the core MCP specification 62 | 63 | 64 | ## `llms.txt` 65 | 66 | This documentation is also available in [llms.txt format](https://llmstxt.org/), which is a simple markdown standard that LLMs can consume easily. 67 | 68 | There are two ways to access the LLM-friendly documentation: 69 | - [`llms.txt`](https://gofastmcp.com/llms.txt) is essentially a sitemap, listing all the pages in the documentation. 70 | - [`llms-full.txt`](https://gofastmcp.com/llms-full.txt) contains the entire documentation. Note this may exceed the context window of your LLM. 71 | -------------------------------------------------------------------------------- /docs/patterns/contrib.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Contrib Modules" 3 | description: "Community-contributed modules extending FastMCP" 4 | icon: "cubes" 5 | --- 6 | 7 | import { VersionBadge } from "/snippets/version-badge.mdx" 8 | 9 | 10 | 11 | FastMCP includes a `contrib` package that holds community-contributed modules. These modules extend FastMCP's functionality but aren't officially maintained by the core team. 12 | 13 | Contrib modules provide additional features, integrations, or patterns that complement the core FastMCP library. They offer a way for the community to share useful extensions while keeping the core library focused and maintainable. 14 | 15 | The available modules can be viewed in the [contrib directory](https://github.com/jlowin/fastmcp/tree/main/src/contrib). 16 | 17 | ## Usage 18 | 19 | To use a contrib module, import it from the `fastmcp.contrib` package: 20 | 21 | ```python 22 | from fastmcp.contrib import my_module 23 | ``` 24 | 25 | ## Important Considerations 26 | 27 | - **Stability**: Modules in `contrib` may have different testing requirements or stability guarantees compared to the core library. 28 | - **Compatibility**: Changes to core FastMCP might break modules in `contrib` without explicit warnings in the main changelog. 29 | - **Dependencies**: Contrib modules may have additional dependencies not required by the core library. These dependencies are typically documented in the module's README or separate requirements files. 30 | 31 | ## Contributing 32 | 33 | We welcome contributions to the `contrib` package! If you have a module that extends FastMCP in a useful way, consider contributing it: 34 | 35 | 1. Create a new directory in `src/fastmcp/contrib/` for your module 36 | 3. Add proper tests for your module in `tests/contrib/` 37 | 2. Include comprehensive documentation in a README.md file, including usage and examples, as well as any additional dependencies or installation instructions 38 | 5. Submit a pull request 39 | 40 | The ideal contrib module: 41 | - Solves a specific use case or integration need 42 | - Follows FastMCP coding standards 43 | - Includes thorough documentation and examples 44 | - Has comprehensive tests 45 | - Specifies any additional dependencies 46 | -------------------------------------------------------------------------------- /docs/patterns/fastapi.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: FastAPI Integration 3 | sidebarTitle: FastAPI 4 | description: Generate MCP servers from FastAPI apps 5 | icon: square-bolt 6 | --- 7 | import { VersionBadge } from '/snippets/version-badge.mdx' 8 | 9 | 10 | 11 | 12 | **Documentation Moved**: The comprehensive FastAPI integration documentation has been moved to the [OpenAPI Integration](/patterns/openapi#fastapi-integration) page, where it's covered alongside all other OpenAPI features including route mapping and tags support. 13 | 14 | 15 | ## Quick Start 16 | 17 | FastMCP can automatically convert FastAPI applications into MCP servers: 18 | 19 | ```python 20 | from fastapi import FastAPI 21 | from fastmcp import FastMCP 22 | 23 | # A FastAPI app 24 | app = FastAPI() 25 | 26 | @app.get("/items") 27 | def list_items(): 28 | return [{"id": 1, "name": "Item 1"}, {"id": 2, "name": "Item 2"}] 29 | 30 | @app.get("/items/{item_id}") 31 | def get_item(item_id: int): 32 | return {"id": item_id, "name": f"Item {item_id}"} 33 | 34 | @app.post("/items") 35 | def create_item(name: str): 36 | return {"id": 3, "name": name} 37 | 38 | # Create an MCP server from your FastAPI app 39 | mcp = FastMCP.from_fastapi(app=app) 40 | 41 | if __name__ == "__main__": 42 | mcp.run() # Start the MCP server 43 | ``` 44 | 45 | 46 | For complete documentation including tag-based routing, route mapping configuration, timeout settings, authentication examples, and advanced configuration options, see the comprehensive [OpenAPI Integration documentation](/patterns/openapi#fastapi-integration). 47 | -------------------------------------------------------------------------------- /docs/patterns/http-requests.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: HTTP Requests 3 | sidebarTitle: HTTP Requests 4 | description: Accessing and using HTTP requests in FastMCP servers 5 | icon: network-wired 6 | --- 7 | import { VersionBadge } from '/snippets/version-badge.mdx' 8 | 9 | 10 | 11 | ## Overview 12 | 13 | When running FastMCP as a web server, your MCP tools, resources, and prompts might need to access the underlying HTTP request information, such as headers, client IP, or query parameters. 14 | 15 | FastMCP provides a clean way to access HTTP request information through a dependency function. 16 | 17 | ## Accessing HTTP Requests 18 | 19 | The recommended way to access the current HTTP request is through the `get_http_request()` dependency function: 20 | 21 | ```python {2, 3, 11} 22 | from fastmcp import FastMCP 23 | from fastmcp.server.dependencies import get_http_request 24 | from starlette.requests import Request 25 | 26 | mcp = FastMCP(name="HTTP Request Demo") 27 | 28 | @mcp.tool() 29 | async def user_agent_info() -> dict: 30 | """Return information about the user agent.""" 31 | # Get the HTTP request 32 | request: Request = get_http_request() 33 | 34 | # Access request data 35 | user_agent = request.headers.get("user-agent", "Unknown") 36 | client_ip = request.client.host if request.client else "Unknown" 37 | 38 | return { 39 | "user_agent": user_agent, 40 | "client_ip": client_ip, 41 | "path": request.url.path, 42 | } 43 | ``` 44 | 45 | This approach works anywhere within a request's execution flow, not just within your MCP function. It's useful when: 46 | 47 | 1. You need access to HTTP information in helper functions 48 | 2. You're calling nested functions that need HTTP request data 49 | 3. You're working with middleware or other request processing code 50 | 51 | ## Accessing HTTP Headers Only 52 | 53 | If you only need request headers and want to avoid potential errors, you can use the `get_http_headers()` helper: 54 | 55 | ```python {2} 56 | from fastmcp import FastMCP 57 | from fastmcp.server.dependencies import get_http_headers 58 | 59 | mcp = FastMCP(name="Headers Demo") 60 | 61 | @mcp.tool() 62 | async def safe_header_info() -> dict: 63 | """Safely get header information without raising errors.""" 64 | # Get headers (returns empty dict if no request context) 65 | headers = get_http_headers() 66 | 67 | # Get authorization header 68 | auth_header = headers.get("authorization", "") 69 | is_bearer = auth_header.startswith("Bearer ") 70 | 71 | return { 72 | "user_agent": headers.get("user-agent", "Unknown"), 73 | "content_type": headers.get("content-type", "Unknown"), 74 | "has_auth": bool(auth_header), 75 | "auth_type": "Bearer" if is_bearer else "Other" if auth_header else "None", 76 | "headers_count": len(headers) 77 | } 78 | ``` 79 | 80 | By default, `get_http_headers()` excludes problematic headers like `host` and `content-length`. To include all headers, use `get_http_headers(include_all=True)`. 81 | 82 | ## Important Notes 83 | 84 | - HTTP requests are only available when FastMCP is running as part of a web application 85 | - Accessing the HTTP request with `get_http_request()` outside of a web request context will raise a `RuntimeError` 86 | - The `get_http_headers()` function **never raises errors** - it returns an empty dict when no request context is available 87 | - The `get_http_request()` function returns a standard [Starlette Request](https://www.starlette.io/requests/) object -------------------------------------------------------------------------------- /docs/patterns/testing.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Testing MCP Servers 3 | sidebarTitle: Testing 4 | description: Learn how to test your FastMCP servers effectively 5 | icon: vial 6 | --- 7 | 8 | 9 | Testing your MCP servers thoroughly is essential for ensuring they work correctly when deployed. FastMCP makes this easy through a variety of testing patterns. 10 | 11 | ## In-Memory Testing 12 | 13 | The most efficient way to test an MCP server is to pass your FastMCP server instance directly to a Client. This enables in-memory testing without having to start a separate server process, which is particularly useful because managing an MCP server programmatically can be challenging. 14 | 15 | Here is an example of using a `Client` to test a server with pytest: 16 | 17 | ```python 18 | import pytest 19 | from fastmcp import FastMCP, Client 20 | 21 | @pytest.fixture 22 | def mcp_server(): 23 | server = FastMCP("TestServer") 24 | 25 | @server.tool() 26 | def greet(name: str) -> str: 27 | return f"Hello, {name}!" 28 | 29 | return server 30 | 31 | async def test_tool_functionality(mcp_server): 32 | # Pass the server directly to the Client constructor 33 | async with Client(mcp_server) as client: 34 | result = await client.call_tool("greet", {"name": "World"}) 35 | assert result[0].text == "Hello, World!" 36 | ``` 37 | 38 | This pattern creates a direct connection between the client and server, allowing you to test your server's functionality efficiently. 39 | -------------------------------------------------------------------------------- /docs/snippets/version-badge.mdx: -------------------------------------------------------------------------------- 1 | export const VersionBadge = ({ version }) => { 2 | return ( 3 | 4 |
5 | New in version:  6 | {version} 7 |
8 |
9 | 10 | 11 | 12 | ); 13 | }; -------------------------------------------------------------------------------- /docs/style.css: -------------------------------------------------------------------------------- 1 | /* Code highlighting -- target only inline code elements, not code blocks */ 2 | p code:not(pre code), 3 | table code:not(pre code), 4 | li code:not(pre code), 5 | h1 code:not(pre code), 6 | h2 code:not(pre code), 7 | h3 code:not(pre code), 8 | h4 code:not(pre code), 9 | h5 code:not(pre code), 10 | h6 code:not(pre code) { 11 | color: #f72585 !important; 12 | background-color: rgba(247, 37, 133, 0.09); 13 | } 14 | 15 | /* Version badge -- display a badge with the current version of the documentation */ 16 | .version-badge { 17 | display: inline-block; 18 | align-items: center; 19 | gap: 0.3em; 20 | padding: 0.2em 0.8em; 21 | font-size: 1.1em; 22 | font-weight: 400; 23 | 24 | font-family: "Inter", sans-serif; 25 | letter-spacing: 0.025em; 26 | color: #ff5400; 27 | background: #ffeee6; 28 | border: 1px solid rgb(255, 84, 0, 0.5); 29 | border-radius: 6px; 30 | box-shadow: none; 31 | vertical-align: middle; 32 | position: relative; 33 | transition: box-shadow 0.2s, transform 0.15s; 34 | } 35 | 36 | .version-badge-container { 37 | margin: 0; 38 | padding: 0; 39 | } 40 | 41 | .version-badge:hover { 42 | box-shadow: 0 2px 8px 0 rgba(160, 132, 252, 0.1); 43 | transform: translateY(-1px) scale(1.03); 44 | } 45 | 46 | .dark .version-badge { 47 | color: #fff; 48 | background: #312e81; 49 | border: 1.5px solid #a78bfa; 50 | } 51 | -------------------------------------------------------------------------------- /examples/complex_inputs.py: -------------------------------------------------------------------------------- 1 | """ 2 | FastMCP Complex inputs Example 3 | 4 | Demonstrates validation via pydantic with complex models. 5 | """ 6 | 7 | from typing import Annotated 8 | 9 | from pydantic import BaseModel, Field 10 | 11 | from fastmcp import FastMCP 12 | 13 | mcp = FastMCP("Shrimp Tank") 14 | 15 | 16 | class ShrimpTank(BaseModel): 17 | class Shrimp(BaseModel): 18 | name: Annotated[str, Field(max_length=10)] 19 | 20 | shrimp: list[Shrimp] 21 | 22 | 23 | @mcp.tool() 24 | def name_shrimp( 25 | tank: ShrimpTank, 26 | # You can use pydantic Field in function signatures for validation. 27 | extra_names: Annotated[list[str], Field(max_length=10)], 28 | ) -> list[str]: 29 | """List all shrimp names in the tank""" 30 | return [shrimp.name for shrimp in tank.shrimp] + extra_names 31 | -------------------------------------------------------------------------------- /examples/desktop.py: -------------------------------------------------------------------------------- 1 | """ 2 | FastMCP Desktop Example 3 | 4 | A simple example that exposes the desktop directory as a resource. 5 | """ 6 | 7 | from pathlib import Path 8 | 9 | from fastmcp import FastMCP 10 | 11 | # Create server 12 | mcp = FastMCP("Demo") 13 | 14 | 15 | @mcp.resource("dir://desktop") 16 | def desktop() -> list[str]: 17 | """List the files in the user's desktop""" 18 | desktop = Path.home() / "Desktop" 19 | return [str(f) for f in desktop.iterdir()] 20 | 21 | 22 | # Add a dynamic greeting resource 23 | @mcp.resource("greeting://{name}") 24 | def get_greeting(name: str) -> str: 25 | """Get a personalized greeting""" 26 | return f"Hello, {name}!" 27 | 28 | 29 | @mcp.tool() 30 | def add(a: int, b: int) -> int: 31 | """Add two numbers""" 32 | return a + b 33 | -------------------------------------------------------------------------------- /examples/echo.py: -------------------------------------------------------------------------------- 1 | """ 2 | FastMCP Echo Server 3 | """ 4 | 5 | from fastmcp import FastMCP 6 | 7 | # Create server 8 | mcp = FastMCP("Echo Server") 9 | 10 | 11 | @mcp.tool() 12 | def echo_tool(text: str) -> str: 13 | """Echo the input text""" 14 | return text 15 | 16 | 17 | @mcp.resource("echo://static") 18 | def echo_resource() -> str: 19 | return "Echo!" 20 | 21 | 22 | @mcp.resource("echo://{text}") 23 | def echo_template(text: str) -> str: 24 | """Echo the input text""" 25 | return f"Echo: {text}" 26 | 27 | 28 | @mcp.prompt("echo") 29 | def echo_prompt(text: str) -> str: 30 | return text 31 | -------------------------------------------------------------------------------- /examples/in_memory_proxy_example.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example demonstrates how to set up and use an in-memory FastMCP proxy. 3 | 4 | It illustrates the pattern: 5 | 1. Create an original FastMCP server with some tools. 6 | 2. Create a proxy FastMCP server using ``FastMCP.as_proxy(original_server)``. 7 | 3. Use another Client to connect to the proxy server (in-memory) and interact with the original server's tools through the proxy. 8 | """ 9 | 10 | import asyncio 11 | 12 | from mcp.types import TextContent 13 | 14 | from fastmcp import FastMCP 15 | from fastmcp.client import Client 16 | 17 | 18 | class EchoService: 19 | """A simple service to demonstrate with""" 20 | 21 | def echo(self, message: str) -> str: 22 | return f"Original server echoes: {message}" 23 | 24 | 25 | async def main(): 26 | print("--- In-Memory FastMCP Proxy Example ---") 27 | print("This example will walk through setting up an in-memory proxy.") 28 | print("-----------------------------------------") 29 | 30 | # 1. Original Server Setup 31 | print( 32 | "\nStep 1: Setting up the Original Server (OriginalEchoServer) with an 'echo' tool..." 33 | ) 34 | original_server = FastMCP("OriginalEchoServer") 35 | original_server.add_tool(EchoService().echo) 36 | print(f" -> Original Server '{original_server.name}' created.") 37 | 38 | # 2. Proxy Server Creation 39 | print("\nStep 2: Creating the Proxy Server (InMemoryProxy)...") 40 | print( 41 | f" (Using FastMCP.as_proxy to wrap '{original_server.name}' directly)" 42 | ) 43 | proxy_server = FastMCP.as_proxy(original_server, name="InMemoryProxy") 44 | print( 45 | f" -> Proxy Server '{proxy_server.name}' created, proxying '{original_server.name}'." 46 | ) 47 | 48 | # 3. Interacting via Proxy 49 | print("\nStep 3: Using a new Client to connect to the Proxy Server and interact...") 50 | async with Client(proxy_server) as final_client: 51 | print(f" -> Successfully connected to proxy '{proxy_server.name}'.") 52 | 53 | print("\n Listing tools available via proxy...") 54 | tools = await final_client.list_tools() 55 | if tools: 56 | print(" Available Tools:") 57 | for tool in tools: 58 | print( 59 | f" - {tool.name} (Description: {tool.description or 'N/A'})" 60 | ) 61 | else: 62 | print(" No tools found via proxy.") 63 | 64 | message_to_echo = "Hello, simplified proxied world!" 65 | print(f"\n Calling 'echo' tool via proxy with message: '{message_to_echo}'") 66 | try: 67 | result = await final_client.call_tool("echo", {"message": message_to_echo}) 68 | if result and isinstance(result[0], TextContent): 69 | print(f" Result from proxied 'echo' call: '{result[0].text}'") 70 | else: 71 | print( 72 | f" Error: Unexpected result format from proxied 'echo' call: {result}" 73 | ) 74 | except Exception as e: 75 | print(f" Error calling 'echo' tool via proxy: {e}") 76 | 77 | print("\n-----------------------------------------") 78 | print("--- In-Memory Proxy Example Finished ---") 79 | 80 | 81 | if __name__ == "__main__": 82 | asyncio.run(main()) 83 | -------------------------------------------------------------------------------- /examples/mount_example.py: -------------------------------------------------------------------------------- 1 | """Example of mounting FastMCP apps together. 2 | 3 | This example demonstrates how to mount FastMCP apps together using 4 | the ToolManager's import_tools functionality. It shows how to: 5 | 6 | 1. Create sub-applications for different domains 7 | 2. Mount those sub-applications to a main application 8 | 3. Access tools with prefixed names and resources with prefixed URIs 9 | """ 10 | 11 | import asyncio 12 | 13 | from fastmcp import FastMCP 14 | 15 | # Weather sub-application 16 | weather_app = FastMCP("Weather App") 17 | 18 | 19 | @weather_app.tool() 20 | def get_weather_forecast(location: str) -> str: 21 | """Get the weather forecast for a location.""" 22 | return f"Sunny skies for {location} today!" 23 | 24 | 25 | @weather_app.resource(uri="weather://forecast") 26 | async def weather_data(): 27 | """Return current weather data.""" 28 | return {"temperature": 72, "conditions": "sunny", "humidity": 45, "wind_speed": 5} 29 | 30 | 31 | # News sub-application 32 | news_app = FastMCP("News App") 33 | 34 | 35 | @news_app.tool() 36 | def get_news_headlines() -> list[str]: 37 | """Get the latest news headlines.""" 38 | return [ 39 | "Tech company launches new product", 40 | "Local team wins championship", 41 | "Scientists make breakthrough discovery", 42 | ] 43 | 44 | 45 | @news_app.resource(uri="news://headlines") 46 | async def news_data(): 47 | """Return latest news data.""" 48 | return { 49 | "top_story": "Breaking news: Important event happened", 50 | "categories": ["politics", "sports", "technology"], 51 | "sources": ["AP", "Reuters", "Local Sources"], 52 | } 53 | 54 | 55 | # Main application 56 | app = FastMCP( 57 | "Main App", dependencies=["fastmcp@git+https://github.com/jlowin/fastmcp.git"] 58 | ) 59 | 60 | 61 | @app.tool() 62 | def check_app_status() -> dict[str, str]: 63 | """Check the status of the main application.""" 64 | return {"status": "running", "version": "1.0.0", "uptime": "3h 24m"} 65 | 66 | 67 | # Mount sub-applications 68 | app.mount("weather", weather_app) 69 | 70 | app.mount("news", news_app) 71 | 72 | 73 | async def get_server_details(): 74 | """Print information about mounted resources.""" 75 | # Print available tools 76 | tools = app._tool_manager.list_tools() 77 | print(f"\nAvailable tools ({len(tools)}):") 78 | for tool in tools: 79 | print(f" - {tool.name}: {tool.description}") 80 | 81 | # Print available resources 82 | print("\nAvailable resources:") 83 | 84 | # Distinguish between native and imported resources 85 | # Native resources would be those directly in the main app (not prefixed) 86 | native_resources = [ 87 | uri 88 | for uri in app._resource_manager._resources 89 | if not (uri.startswith("weather+") or uri.startswith("news+")) 90 | ] 91 | 92 | # Imported resources - categorized by source app 93 | weather_resources = [ 94 | uri for uri in app._resource_manager._resources if uri.startswith("weather+") 95 | ] 96 | news_resources = [ 97 | uri for uri in app._resource_manager._resources if uri.startswith("news+") 98 | ] 99 | 100 | print(f" - Native app resources: {native_resources}") 101 | print(f" - Imported from weather app: {weather_resources}") 102 | print(f" - Imported from news app: {news_resources}") 103 | 104 | # Let's try to access resources using the prefixed URI 105 | weather_data = await app.read_resource("weather+weather://forecast") 106 | print(f"\nWeather data from prefixed URI: {weather_data}") 107 | 108 | 109 | if __name__ == "__main__": 110 | # First run our async function to display info 111 | asyncio.run(get_server_details()) 112 | 113 | # Then start the server (uncomment to run the server) 114 | app.run() 115 | -------------------------------------------------------------------------------- /examples/sampling.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example of using sampling to request an LLM completion via Marvin 3 | """ 4 | 5 | import asyncio 6 | 7 | import marvin 8 | from mcp.types import TextContent 9 | 10 | from fastmcp import Client, Context, FastMCP 11 | from fastmcp.client.sampling import RequestContext, SamplingMessage, SamplingParams 12 | 13 | # -- Create a server that sends a sampling request to the LLM 14 | 15 | mcp = FastMCP("Sampling Example") 16 | 17 | 18 | @mcp.tool() 19 | async def example_tool(prompt: str, context: Context) -> str: 20 | """Sample a completion from the LLM.""" 21 | response = await context.sample( 22 | "What is your favorite programming language?", 23 | system_prompt="You love languages named after snakes.", 24 | ) 25 | assert isinstance(response, TextContent) 26 | return response.text 27 | 28 | 29 | # -- Create a client that can handle the sampling request 30 | 31 | 32 | async def sampling_fn( 33 | messages: list[SamplingMessage], 34 | params: SamplingParams, 35 | ctx: RequestContext, 36 | ) -> str: 37 | return await marvin.say_async( 38 | message=[m.content.text for m in messages], 39 | instructions=params.systemPrompt, 40 | ) 41 | 42 | 43 | async def run(): 44 | async with Client(mcp, sampling_handler=sampling_fn) as client: 45 | result = await client.call_tool( 46 | "example_tool", {"prompt": "What is the best programming language?"} 47 | ) 48 | print(result) 49 | 50 | 51 | if __name__ == "__main__": 52 | asyncio.run(run()) 53 | -------------------------------------------------------------------------------- /examples/screenshot.py: -------------------------------------------------------------------------------- 1 | """ 2 | FastMCP Screenshot Example 3 | 4 | Give Claude a tool to capture and view screenshots. 5 | """ 6 | 7 | import io 8 | 9 | from fastmcp import FastMCP, Image 10 | 11 | # Create server 12 | mcp = FastMCP("Screenshot Demo", dependencies=["pyautogui", "Pillow"]) 13 | 14 | 15 | @mcp.tool() 16 | def take_screenshot() -> Image: 17 | """ 18 | Take a screenshot of the user's screen and return it as an image. Use 19 | this tool anytime the user wants you to look at something they're doing. 20 | """ 21 | import pyautogui 22 | 23 | buffer = io.BytesIO() 24 | 25 | # if the file exceeds ~1MB, it will be rejected by Claude 26 | screenshot = pyautogui.screenshot() 27 | screenshot.convert("RGB").save(buffer, format="JPEG", quality=60, optimize=True) 28 | return Image(data=buffer.getvalue(), format="jpeg") 29 | -------------------------------------------------------------------------------- /examples/serializer.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Any 3 | 4 | import yaml 5 | 6 | from fastmcp import FastMCP 7 | 8 | 9 | # Define a simple custom serializer 10 | def custom_dict_serializer(data: Any) -> str: 11 | return yaml.dump(data, width=100, sort_keys=False) 12 | 13 | 14 | server = FastMCP(name="CustomSerializerExample", tool_serializer=custom_dict_serializer) 15 | 16 | 17 | @server.tool() 18 | def get_example_data() -> dict: 19 | """Returns some example data.""" 20 | return {"name": "Test", "value": 123, "status": True} 21 | 22 | 23 | async def example_usage(): 24 | result = await server._mcp_call_tool("get_example_data", {}) 25 | print("Tool Result:") 26 | print(result) 27 | print("This is an example of using a custom serializer with FastMCP.") 28 | 29 | 30 | if __name__ == "__main__": 31 | asyncio.run(example_usage()) 32 | server.run() 33 | -------------------------------------------------------------------------------- /examples/simple_echo.py: -------------------------------------------------------------------------------- 1 | """ 2 | FastMCP Echo Server 3 | """ 4 | 5 | from fastmcp import FastMCP 6 | 7 | # Create server 8 | mcp = FastMCP("Echo Server") 9 | 10 | 11 | @mcp.tool() 12 | def echo(text: str) -> str: 13 | """Echo the input text""" 14 | return text 15 | -------------------------------------------------------------------------------- /examples/smart_home/README.md: -------------------------------------------------------------------------------- 1 | # smart home mcp server 2 | 3 | ```bash 4 | cd examples/smart_home 5 | mcp install src/smart_home/hub.py:hub_mcp -f .env 6 | ``` 7 | where `.env` contains the following: 8 | ``` 9 | HUE_BRIDGE_IP= 10 | HUE_BRIDGE_USERNAME= 11 | ``` 12 | 13 | ```bash 14 | open -a Claude 15 | ``` -------------------------------------------------------------------------------- /examples/smart_home/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "smart-home" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | authors = [{ name = "zzstoatzz", email = "thrast36@gmail.com" }] 7 | requires-python = ">=3.12" 8 | dependencies = ["fastmcp@git+https://github.com/jlowin/fastmcp.git", "phue2"] 9 | 10 | [project.scripts] 11 | smart-home = "smart_home.__main__:main" 12 | 13 | [dependency-groups] 14 | dev = ["ruff", "ipython"] 15 | 16 | 17 | [build-system] 18 | requires = ["uv_build"] 19 | build-backend = "uv_build" 20 | -------------------------------------------------------------------------------- /examples/smart_home/src/smart_home/__init__.py: -------------------------------------------------------------------------------- 1 | from smart_home.settings import settings 2 | 3 | __all__ = ["settings"] 4 | -------------------------------------------------------------------------------- /examples/smart_home/src/smart_home/__main__.py: -------------------------------------------------------------------------------- 1 | from smart_home.hub import hub_mcp 2 | 3 | 4 | def main(): 5 | hub_mcp.run() 6 | 7 | 8 | if __name__ == "__main__": 9 | main() 10 | -------------------------------------------------------------------------------- /examples/smart_home/src/smart_home/hub.py: -------------------------------------------------------------------------------- 1 | from phue2 import Bridge 2 | 3 | from fastmcp import FastMCP 4 | from smart_home.lights.server import lights_mcp 5 | from smart_home.settings import settings 6 | 7 | hub_mcp = FastMCP( 8 | "Smart Home Hub (phue2)", 9 | dependencies=[ 10 | "smart_home@git+https://github.com/jlowin/fastmcp.git#subdirectory=examples/smart_home", 11 | ], 12 | ) 13 | 14 | # Mount the lights service under the 'hue' prefix 15 | hub_mcp.mount("hue", lights_mcp) 16 | 17 | 18 | # Add a status check for the hub 19 | @hub_mcp.tool() 20 | def hub_status() -> str: 21 | """Checks the status of the main hub and connections.""" 22 | try: 23 | bridge = Bridge( 24 | ip=str(settings.hue_bridge_ip), 25 | username=settings.hue_bridge_username, 26 | save_config=False, 27 | ) 28 | bridge.connect() 29 | return "Hub OK. Hue Bridge Connected (via phue2)." 30 | except Exception as e: 31 | return f"Hub Warning: Hue Bridge connection failed or not attempted: {e}" 32 | 33 | 34 | # Add mounting points for other services later 35 | # hub_mcp.mount("thermo", thermostat_mcp) 36 | -------------------------------------------------------------------------------- /examples/smart_home/src/smart_home/lights/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlowin/fastmcp/97ef80c62478d0f366d62baf3e15875937cd7b9f/examples/smart_home/src/smart_home/lights/__init__.py -------------------------------------------------------------------------------- /examples/smart_home/src/smart_home/lights/hue_utils.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from phue2 import Bridge 4 | from phue2.exceptions import PhueException 5 | 6 | from smart_home.settings import settings 7 | 8 | 9 | def _get_bridge() -> Bridge | None: 10 | """Attempts to connect to the Hue bridge using settings.""" 11 | try: 12 | return Bridge( 13 | ip=str(settings.hue_bridge_ip), 14 | username=settings.hue_bridge_username, 15 | save_config=False, 16 | ) 17 | except Exception: 18 | # Broad exception to catch potential connection issues 19 | # TODO: Add more specific logging or error handling 20 | return None 21 | 22 | 23 | def handle_phue_error( 24 | light_or_group: str, operation: str, error: Exception 25 | ) -> dict[str, Any]: 26 | """Creates a standardized error response for phue2 operations.""" 27 | base_info = {"target": light_or_group, "operation": operation, "success": False} 28 | if isinstance(error, KeyError): 29 | base_info["error"] = f"Target '{light_or_group}' not found" 30 | elif isinstance(error, PhueException): 31 | base_info["error"] = f"phue2 error during {operation}: {error}" 32 | else: 33 | base_info["error"] = f"Unexpected error during {operation}: {error}" 34 | return base_info 35 | -------------------------------------------------------------------------------- /examples/smart_home/src/smart_home/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlowin/fastmcp/97ef80c62478d0f366d62baf3e15875937cd7b9f/examples/smart_home/src/smart_home/py.typed -------------------------------------------------------------------------------- /examples/smart_home/src/smart_home/settings.py: -------------------------------------------------------------------------------- 1 | from pydantic import Field, IPvAnyAddress 2 | from pydantic_settings import BaseSettings, SettingsConfigDict 3 | 4 | 5 | class Settings(BaseSettings): 6 | model_config = SettingsConfigDict(env_file=".env", extra="ignore") 7 | 8 | hue_bridge_ip: IPvAnyAddress = Field(default=...) 9 | hue_bridge_username: str = Field(default=...) 10 | 11 | 12 | settings = Settings() 13 | -------------------------------------------------------------------------------- /examples/tags_example.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example demonstrating RouteMap tags functionality. 3 | 4 | This example shows how to use the tags parameter in RouteMap 5 | to selectively route OpenAPI endpoints based on their tags. 6 | """ 7 | 8 | import asyncio 9 | 10 | from fastapi import FastAPI 11 | 12 | from fastmcp import FastMCP 13 | from fastmcp.server.openapi import MCPType, RouteMap 14 | 15 | # Create a FastAPI app with tagged endpoints 16 | app = FastAPI(title="Tagged API Example") 17 | 18 | 19 | @app.get("/users", tags=["users", "public"]) 20 | async def get_users(): 21 | """Get all users - public endpoint""" 22 | return [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}] 23 | 24 | 25 | @app.post("/users", tags=["users", "admin"]) 26 | async def create_user(name: str): 27 | """Create a user - admin only""" 28 | return {"id": 3, "name": name} 29 | 30 | 31 | @app.get("/admin/stats", tags=["admin", "internal"]) 32 | async def get_admin_stats(): 33 | """Get admin statistics - internal use""" 34 | return {"total_users": 100, "active_sessions": 25} 35 | 36 | 37 | @app.get("/health", tags=["public"]) 38 | async def health_check(): 39 | """Public health check""" 40 | return {"status": "healthy"} 41 | 42 | 43 | @app.get("/metrics") 44 | async def get_metrics(): 45 | """Metrics endpoint with no tags""" 46 | return {"requests": 1000, "errors": 5} 47 | 48 | 49 | async def main(): 50 | """Demonstrate different tag-based routing strategies.""" 51 | 52 | print("=== Example 1: Make admin-tagged routes tools ===") 53 | 54 | # Strategy 1: Convert admin-tagged routes to tools 55 | mcp1 = FastMCP.from_fastapi( 56 | app=app, 57 | route_maps=[ 58 | RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL, tags={"admin"}), 59 | RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.RESOURCE), 60 | ], 61 | ) 62 | 63 | tools = await mcp1.get_tools() 64 | resources = await mcp1.get_resources() 65 | 66 | print(f"Tools ({len(tools)}): {', '.join(tools.keys())}") 67 | print(f"Resources ({len(resources)}): {', '.join(resources.keys())}") 68 | 69 | print("\n=== Example 2: Exclude internal routes ===") 70 | 71 | # Strategy 2: Exclude internal routes entirely 72 | mcp2 = FastMCP.from_fastapi( 73 | app=app, 74 | route_maps=[ 75 | RouteMap( 76 | methods="*", pattern=r".*", mcp_type=MCPType.EXCLUDE, tags={"internal"} 77 | ), 78 | RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.RESOURCE), 79 | RouteMap(methods=["POST"], pattern=r".*", mcp_type=MCPType.TOOL), 80 | ], 81 | ) 82 | 83 | tools = await mcp2.get_tools() 84 | resources = await mcp2.get_resources() 85 | 86 | print(f"Tools ({len(tools)}): {', '.join(tools.keys())}") 87 | print(f"Resources ({len(resources)}): {', '.join(resources.keys())}") 88 | 89 | print("\n=== Example 3: Pattern + Tags combination ===") 90 | 91 | # Strategy 3: Routes matching both pattern AND tags 92 | mcp3 = FastMCP.from_fastapi( 93 | app=app, 94 | route_maps=[ 95 | # Admin routes under /admin path -> tools 96 | RouteMap( 97 | methods="*", 98 | pattern=r".*/admin/.*", 99 | mcp_type=MCPType.TOOL, 100 | tags={"admin"}, 101 | ), 102 | # Public routes -> tools 103 | RouteMap( 104 | methods="*", pattern=r".*", mcp_type=MCPType.TOOL, tags={"public"} 105 | ), 106 | RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.RESOURCE), 107 | ], 108 | ) 109 | 110 | tools = await mcp3.get_tools() 111 | resources = await mcp3.get_resources() 112 | 113 | print(f"Tools ({len(tools)}): {', '.join(tools.keys())}") 114 | print(f"Resources ({len(resources)}): {', '.join(resources.keys())}") 115 | 116 | print("\n=== Example 4: Multiple tag AND condition ===") 117 | 118 | # Strategy 4: Routes must have ALL specified tags 119 | mcp4 = FastMCP.from_fastapi( 120 | app=app, 121 | route_maps=[ 122 | # Routes with BOTH "users" AND "admin" tags -> tools 123 | RouteMap( 124 | methods="*", 125 | pattern=r".*", 126 | mcp_type=MCPType.TOOL, 127 | tags={"users", "admin"}, 128 | ), 129 | RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.RESOURCE), 130 | ], 131 | ) 132 | 133 | tools = await mcp4.get_tools() 134 | resources = await mcp4.get_resources() 135 | 136 | print(f"Tools ({len(tools)}): {', '.join(tools.keys())}") 137 | print(f"Resources ({len(resources)}): {', '.join(resources.keys())}") 138 | 139 | 140 | if __name__ == "__main__": 141 | asyncio.run(main()) 142 | -------------------------------------------------------------------------------- /examples/text_me.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # dependencies = ["fastmcp"] 3 | # /// 4 | 5 | """ 6 | FastMCP Text Me Server 7 | -------------------------------- 8 | This defines a simple FastMCP server that sends a text message to a phone number via https://surgemsg.com/. 9 | 10 | To run this example, create a `.env` file with the following values: 11 | 12 | SURGE_API_KEY=... 13 | SURGE_ACCOUNT_ID=... 14 | SURGE_MY_PHONE_NUMBER=... 15 | SURGE_MY_FIRST_NAME=... 16 | SURGE_MY_LAST_NAME=... 17 | 18 | Visit https://surgemsg.com/ and click "Get Started" to obtain these values. 19 | """ 20 | 21 | from typing import Annotated 22 | 23 | import httpx 24 | from pydantic import BeforeValidator 25 | from pydantic_settings import BaseSettings, SettingsConfigDict 26 | 27 | from fastmcp import FastMCP 28 | 29 | 30 | class SurgeSettings(BaseSettings): 31 | model_config: SettingsConfigDict = SettingsConfigDict( 32 | env_prefix="SURGE_", env_file=".env" 33 | ) 34 | 35 | api_key: str 36 | account_id: str 37 | my_phone_number: Annotated[ 38 | str, BeforeValidator(lambda v: "+" + v if not v.startswith("+") else v) 39 | ] 40 | my_first_name: str 41 | my_last_name: str 42 | 43 | 44 | # Create server 45 | mcp = FastMCP("Text me") 46 | surge_settings = SurgeSettings() # type: ignore 47 | 48 | 49 | @mcp.tool(name="textme", description="Send a text message to me") 50 | def text_me(text_content: str) -> str: 51 | """Send a text message to a phone number via https://surgemsg.com/""" 52 | with httpx.Client() as client: 53 | response = client.post( 54 | "https://api.surgemsg.com/messages", 55 | headers={ 56 | "Authorization": f"Bearer {surge_settings.api_key}", 57 | "Surge-Account": surge_settings.account_id, 58 | "Content-Type": "application/json", 59 | }, 60 | json={ 61 | "body": text_content, 62 | "conversation": { 63 | "contact": { 64 | "first_name": surge_settings.my_first_name, 65 | "last_name": surge_settings.my_last_name, 66 | "phone_number": surge_settings.my_phone_number, 67 | } 68 | }, 69 | }, 70 | ) 71 | response.raise_for_status() 72 | return f"Message sent: {text_content}" 73 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | build: 2 | uv sync 3 | 4 | test: build 5 | uv run --frozen pytest -xvs tests 6 | 7 | # Run pyright on all files 8 | typecheck: 9 | uv run --frozen pyright -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "fastmcp" 3 | dynamic = ["version"] 4 | description = "The fast, Pythonic way to build MCP servers." 5 | authors = [{ name = "Jeremiah Lowin" }] 6 | dependencies = [ 7 | "python-dotenv>=1.1.0", 8 | "exceptiongroup>=1.2.2", 9 | "httpx>=0.28.1", 10 | "mcp>=1.9.2,<2.0.0", 11 | "openapi-pydantic>=0.5.1", 12 | "rich>=13.9.4", 13 | "typer>=0.15.2", 14 | "websockets>=14.0", 15 | "authlib>=1.5.2", 16 | ] 17 | requires-python = ">=3.10" 18 | readme = "README.md" 19 | license = "Apache-2.0" 20 | 21 | keywords = [ 22 | "mcp", 23 | "mcp server", 24 | "mcp client", 25 | "model context protocol", 26 | "fastmcp", 27 | "llm", 28 | "agent", 29 | ] 30 | classifiers = [ 31 | "Intended Audience :: Developers", 32 | "License :: OSI Approved :: Apache Software License", 33 | "Topic :: Scientific/Engineering :: Artificial Intelligence", 34 | "Programming Language :: Python :: 3.10", 35 | "Programming Language :: Python :: 3.11", 36 | "Programming Language :: Python :: 3.12", 37 | "Typing :: Typed", 38 | ] 39 | 40 | [dependency-groups] 41 | dev = [ 42 | "copychat>=0.5.2", 43 | "dirty-equals>=0.9.0", 44 | "fastapi>=0.115.12", 45 | "ipython>=8.12.3", 46 | "pdbpp>=0.10.3", 47 | "pre-commit", 48 | "pyinstrument>=5.0.2", 49 | "pyright>=1.1.389", 50 | "pytest>=8.3.3", 51 | "pytest-asyncio>=0.23.5", 52 | "pytest-cov>=6.1.1", 53 | "pytest-env>=1.1.5", 54 | "pytest-flakefinder", 55 | "pytest-httpx>=0.35.0", 56 | "pytest-report>=0.2.1", 57 | "pytest-timeout>=2.4.0", 58 | "pytest-xdist>=3.6.1", 59 | "ruff", 60 | ] 61 | 62 | [project.scripts] 63 | fastmcp = "fastmcp.cli:app" 64 | 65 | [project.urls] 66 | Homepage = "https://gofastmcp.com" 67 | Repository = "https://github.com/jlowin/fastmcp" 68 | Documentation = "https://gofastmcp.com" 69 | 70 | 71 | [build-system] 72 | requires = ["hatchling", "uv-dynamic-versioning>=0.7.0"] 73 | build-backend = "hatchling.build" 74 | 75 | [tool.hatch.version] 76 | source = "uv-dynamic-versioning" 77 | 78 | [tool.uv-dynamic-versioning] 79 | vcs = "git" 80 | style = "pep440" 81 | bump = true 82 | fallback-version = "0.0.0" 83 | 84 | 85 | [tool.pytest.ini_options] 86 | asyncio_mode = "auto" 87 | asyncio_default_fixture_loop_scope = "session" 88 | asyncio_default_test_loop_scope = "session" 89 | filterwarnings = [] 90 | timeout = 3 91 | env = [ 92 | "FASTMCP_TEST_MODE=1", 93 | 'D:FASTMCP_LOG_LEVEL=DEBUG', 94 | 'D:FASTMCP_ENABLE_RICH_TRACEBACKS=0', 95 | ] 96 | 97 | [tool.pyright] 98 | include = ["src", "tests"] 99 | exclude = ["**/node_modules", "**/__pycache__", ".venv", ".git", "dist"] 100 | pythonVersion = "3.10" 101 | pythonPlatform = "Darwin" 102 | typeCheckingMode = "basic" 103 | reportMissingImports = true 104 | reportMissingTypeStubs = false 105 | useLibraryCodeForTypes = true 106 | venvPath = "." 107 | venv = ".venv" 108 | strict = ["src/fastmcp/server/server.py"] 109 | 110 | [tool.ruff.lint] 111 | extend-select = ["I", "UP"] 112 | 113 | [tool.ruff.lint.per-file-ignores] 114 | "__init__.py" = ["F401", "I001", "RUF013"] 115 | -------------------------------------------------------------------------------- /src/fastmcp/__init__.py: -------------------------------------------------------------------------------- 1 | """FastMCP - An ergonomic MCP interface.""" 2 | 3 | from importlib.metadata import version 4 | 5 | from fastmcp.server.server import FastMCP 6 | from fastmcp.server.context import Context 7 | import fastmcp.server 8 | 9 | from fastmcp.client import Client 10 | from fastmcp.utilities.types import Image 11 | from . import client, settings 12 | 13 | __version__ = version("fastmcp") 14 | __all__ = [ 15 | "FastMCP", 16 | "Context", 17 | "client", 18 | "Client", 19 | "settings", 20 | "Image", 21 | ] 22 | -------------------------------------------------------------------------------- /src/fastmcp/cli/__init__.py: -------------------------------------------------------------------------------- 1 | """FastMCP CLI package.""" 2 | 3 | from .cli import app 4 | 5 | if __name__ == "__main__": 6 | app() 7 | -------------------------------------------------------------------------------- /src/fastmcp/client/__init__.py: -------------------------------------------------------------------------------- 1 | from .client import Client 2 | from .transports import ( 3 | ClientTransport, 4 | WSTransport, 5 | SSETransport, 6 | StdioTransport, 7 | PythonStdioTransport, 8 | NodeStdioTransport, 9 | UvxStdioTransport, 10 | NpxStdioTransport, 11 | FastMCPTransport, 12 | StreamableHttpTransport, 13 | ) 14 | from .auth import OAuth, BearerAuth 15 | 16 | __all__ = [ 17 | "Client", 18 | "ClientTransport", 19 | "WSTransport", 20 | "SSETransport", 21 | "StdioTransport", 22 | "PythonStdioTransport", 23 | "NodeStdioTransport", 24 | "UvxStdioTransport", 25 | "NpxStdioTransport", 26 | "FastMCPTransport", 27 | "StreamableHttpTransport", 28 | "OAuth", 29 | "BearerAuth", 30 | ] 31 | -------------------------------------------------------------------------------- /src/fastmcp/client/auth/__init__.py: -------------------------------------------------------------------------------- 1 | from .bearer import BearerAuth 2 | from .oauth import OAuth 3 | 4 | __all__ = ["BearerAuth", "OAuth"] 5 | -------------------------------------------------------------------------------- /src/fastmcp/client/auth/bearer.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | from pydantic import SecretStr 3 | 4 | from fastmcp.utilities.logging import get_logger 5 | 6 | __all__ = ["BearerAuth"] 7 | 8 | logger = get_logger(__name__) 9 | 10 | 11 | class BearerAuth(httpx.Auth): 12 | def __init__(self, token: str): 13 | self.token = SecretStr(token) 14 | 15 | def auth_flow(self, request): 16 | request.headers["Authorization"] = f"Bearer {self.token.get_secret_value()}" 17 | yield request 18 | -------------------------------------------------------------------------------- /src/fastmcp/client/logging.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Awaitable, Callable 2 | from typing import TypeAlias 3 | 4 | from mcp.client.session import LoggingFnT, MessageHandlerFnT 5 | from mcp.types import LoggingMessageNotificationParams 6 | 7 | from fastmcp.utilities.logging import get_logger 8 | 9 | logger = get_logger(__name__) 10 | 11 | LogMessage: TypeAlias = LoggingMessageNotificationParams 12 | LogHandler: TypeAlias = Callable[[LogMessage], Awaitable[None]] 13 | MessageHandler: TypeAlias = MessageHandlerFnT 14 | 15 | 16 | async def default_log_handler(message: LogMessage) -> None: 17 | logger.debug(f"Log received: {message}") 18 | 19 | 20 | def create_log_callback(handler: LogHandler | None = None) -> LoggingFnT: 21 | if handler is None: 22 | handler = default_log_handler 23 | 24 | async def log_callback(params: LoggingMessageNotificationParams) -> None: 25 | await handler(params) 26 | 27 | return log_callback 28 | -------------------------------------------------------------------------------- /src/fastmcp/client/progress.py: -------------------------------------------------------------------------------- 1 | from typing import TypeAlias 2 | 3 | from mcp.shared.session import ProgressFnT 4 | 5 | from fastmcp.utilities.logging import get_logger 6 | 7 | logger = get_logger(__name__) 8 | 9 | ProgressHandler: TypeAlias = ProgressFnT 10 | 11 | 12 | async def default_progress_handler( 13 | progress: float, total: float | None, message: str | None 14 | ) -> None: 15 | """Default handler for progress notifications. 16 | 17 | Logs progress updates at debug level, properly handling missing total or message values. 18 | 19 | Args: 20 | progress: Current progress value 21 | total: Optional total expected value 22 | message: Optional status message 23 | """ 24 | if total is not None: 25 | # We have both progress and total 26 | percent = (progress / total) * 100 27 | progress_str = f"{progress}/{total} ({percent:.1f}%)" 28 | else: 29 | # We only have progress 30 | progress_str = f"{progress}" 31 | 32 | # Include message if available 33 | if message: 34 | log_msg = f"Progress: {progress_str} - {message}" 35 | else: 36 | log_msg = f"Progress: {progress_str}" 37 | 38 | logger.debug(log_msg) 39 | -------------------------------------------------------------------------------- /src/fastmcp/client/roots.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from collections.abc import Awaitable, Callable 3 | from typing import TypeAlias 4 | 5 | import mcp.types 6 | import pydantic 7 | from mcp import ClientSession 8 | from mcp.client.session import ListRootsFnT 9 | from mcp.shared.context import LifespanContextT, RequestContext 10 | 11 | RootsList: TypeAlias = list[str] | list[mcp.types.Root] | list[str | mcp.types.Root] 12 | 13 | RootsHandler: TypeAlias = ( 14 | Callable[[RequestContext[ClientSession, LifespanContextT]], RootsList] 15 | | Callable[[RequestContext[ClientSession, LifespanContextT]], Awaitable[RootsList]] 16 | ) 17 | 18 | 19 | def convert_roots_list(roots: RootsList) -> list[mcp.types.Root]: 20 | roots_list = [] 21 | for r in roots: 22 | if isinstance(r, mcp.types.Root): 23 | roots_list.append(r) 24 | elif isinstance(r, pydantic.FileUrl): 25 | roots_list.append(mcp.types.Root(uri=r)) 26 | elif isinstance(r, str): 27 | roots_list.append(mcp.types.Root(uri=pydantic.FileUrl(r))) 28 | else: 29 | raise ValueError(f"Invalid root: {r}") 30 | return roots_list 31 | 32 | 33 | def create_roots_callback( 34 | handler: RootsList | RootsHandler, 35 | ) -> ListRootsFnT: 36 | if isinstance(handler, list): 37 | return _create_roots_callback_from_roots(handler) 38 | elif inspect.isfunction(handler): 39 | return _create_roots_callback_from_fn(handler) 40 | else: 41 | raise ValueError(f"Invalid roots handler: {handler}") 42 | 43 | 44 | def _create_roots_callback_from_roots( 45 | roots: RootsList, 46 | ) -> ListRootsFnT: 47 | roots = convert_roots_list(roots) 48 | 49 | async def _roots_callback( 50 | context: RequestContext[ClientSession, LifespanContextT], 51 | ) -> mcp.types.ListRootsResult: 52 | return mcp.types.ListRootsResult(roots=roots) 53 | 54 | return _roots_callback 55 | 56 | 57 | def _create_roots_callback_from_fn( 58 | fn: Callable[[RequestContext[ClientSession, LifespanContextT]], RootsList] 59 | | Callable[[RequestContext[ClientSession, LifespanContextT]], Awaitable[RootsList]], 60 | ) -> ListRootsFnT: 61 | async def _roots_callback( 62 | context: RequestContext[ClientSession, LifespanContextT], 63 | ) -> mcp.types.ListRootsResult | mcp.types.ErrorData: 64 | try: 65 | roots = fn(context) 66 | if inspect.isawaitable(roots): 67 | roots = await roots 68 | return mcp.types.ListRootsResult(roots=convert_roots_list(roots)) 69 | except Exception as e: 70 | return mcp.types.ErrorData( 71 | code=mcp.types.INTERNAL_ERROR, 72 | message=str(e), 73 | ) 74 | 75 | return _roots_callback 76 | -------------------------------------------------------------------------------- /src/fastmcp/client/sampling.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from collections.abc import Awaitable, Callable 3 | from typing import TypeAlias 4 | 5 | import mcp.types 6 | from mcp import ClientSession, CreateMessageResult 7 | from mcp.client.session import SamplingFnT 8 | from mcp.shared.context import LifespanContextT, RequestContext 9 | from mcp.types import CreateMessageRequestParams as SamplingParams 10 | from mcp.types import SamplingMessage 11 | 12 | __all__ = ["SamplingMessage", "SamplingParams", "MessageResult", "SamplingHandler"] 13 | 14 | 15 | class MessageResult(CreateMessageResult): 16 | role: mcp.types.Role = "assistant" 17 | content: mcp.types.TextContent | mcp.types.ImageContent 18 | model: str = "client-model" 19 | 20 | 21 | SamplingHandler: TypeAlias = Callable[ 22 | [ 23 | list[SamplingMessage], 24 | SamplingParams, 25 | RequestContext[ClientSession, LifespanContextT], 26 | ], 27 | str | CreateMessageResult | Awaitable[str | CreateMessageResult], 28 | ] 29 | 30 | 31 | def create_sampling_callback(sampling_handler: SamplingHandler) -> SamplingFnT: 32 | async def _sampling_handler( 33 | context: RequestContext[ClientSession, LifespanContextT], 34 | params: SamplingParams, 35 | ) -> CreateMessageResult | mcp.types.ErrorData: 36 | try: 37 | result = sampling_handler(params.messages, params, context) 38 | if inspect.isawaitable(result): 39 | result = await result 40 | 41 | if isinstance(result, str): 42 | result = MessageResult( 43 | content=mcp.types.TextContent(type="text", text=result) 44 | ) 45 | return result 46 | except Exception as e: 47 | return mcp.types.ErrorData( 48 | code=mcp.types.INTERNAL_ERROR, 49 | message=str(e), 50 | ) 51 | 52 | return _sampling_handler 53 | -------------------------------------------------------------------------------- /src/fastmcp/contrib/README.md: -------------------------------------------------------------------------------- 1 | # FastMCP Contrib Modules 2 | 3 | This directory holds community-contributed modules for FastMCP. These modules extend FastMCP's functionality but are not officially maintained by the core team. 4 | 5 | **Guarantees:** 6 | * Modules in `contrib` may have different testing requirements or stability guarantees compared to the core library. 7 | * Changes to the core FastMCP library might break modules in `contrib` without explicit warnings in the main changelog. 8 | 9 | Use these modules at your own discretion. Contributions are welcome, but please include tests and documentation. 10 | 11 | ## Usage 12 | 13 | To use a contrib module, import it from the `fastmcp.contrib` package. 14 | 15 | ```python 16 | from fastmcp.contrib import my_module 17 | ``` 18 | 19 | Note that the contrib modules may have different dependencies than the core library, which can be noted in their respective README's or even separate requirements / dependency files. -------------------------------------------------------------------------------- /src/fastmcp/contrib/bulk_tool_caller/README.md: -------------------------------------------------------------------------------- 1 | # Bulk Tool Caller 2 | 3 | This module provides the `BulkToolCaller` class, which extends the `MCPMixin` to offer tools for performing multiple tool calls in a single request to a FastMCP server. This can be useful for optimizing interactions with the server by reducing the overhead of individual tool calls. 4 | 5 | ## Usage 6 | 7 | To use the `BulkToolCaller`, see the example [example.py](./example.py) file. The `BulkToolCaller` can be instantiated and then registered with a FastMCP server URL. It provides methods to call multiple tools in bulk, either different tools or the same tool with different arguments. 8 | 9 | 10 | ## Provided Tools 11 | 12 | The `BulkToolCaller` provides the following tools: 13 | 14 | ### `call_tools_bulk` 15 | 16 | Calls multiple different tools registered on the MCP server in a single request. 17 | 18 | - **Arguments:** 19 | - `tool_calls` (list of `CallToolRequest`): A list of objects, where each object specifies the `tool` name and `arguments` for an individual tool call. 20 | - `continue_on_error` (bool, optional): If `True`, continue executing subsequent tool calls even if a previous one resulted in an error. Defaults to `True`. 21 | 22 | - **Returns:** 23 | A list of `CallToolRequestResult` objects, each containing the result (`isError`, `content`) and the original `tool` name and `arguments` for each call. 24 | 25 | ### `call_tool_bulk` 26 | 27 | Calls a single tool registered on the MCP server multiple times with different arguments in a single request. 28 | 29 | - **Arguments:** 30 | - `tool` (str): The name of the tool to call. 31 | - `tool_arguments` (list of dict): A list of dictionaries, where each dictionary contains the arguments for an individual run of the tool. 32 | - `continue_on_error` (bool, optional): If `True`, continue executing subsequent tool calls even if a previous one resulted in an error. Defaults to `True`. 33 | 34 | - **Returns:** 35 | A list of `CallToolRequestResult` objects, each containing the result (`isError`, `content`) and the original `tool` name and `arguments` for each call. -------------------------------------------------------------------------------- /src/fastmcp/contrib/bulk_tool_caller/__init__.py: -------------------------------------------------------------------------------- 1 | from .bulk_tool_caller import BulkToolCaller 2 | 3 | __all__ = ["BulkToolCaller"] 4 | -------------------------------------------------------------------------------- /src/fastmcp/contrib/bulk_tool_caller/bulk_tool_caller.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from mcp.types import CallToolResult 4 | from pydantic import BaseModel, Field 5 | 6 | from fastmcp import FastMCP 7 | from fastmcp.client import Client 8 | from fastmcp.client.transports import FastMCPTransport 9 | from fastmcp.contrib.mcp_mixin.mcp_mixin import ( 10 | _DEFAULT_SEPARATOR_TOOL, 11 | MCPMixin, 12 | mcp_tool, 13 | ) 14 | 15 | 16 | class CallToolRequest(BaseModel): 17 | """A class to represent a request to call a tool with specific arguments.""" 18 | 19 | tool: str = Field(description="The name of the tool to call.") 20 | arguments: dict[str, Any] = Field( 21 | description="A dictionary containing the arguments for the tool call." 22 | ) 23 | 24 | 25 | class CallToolRequestResult(CallToolResult): 26 | """ 27 | A class to represent the result of a bulk tool call. 28 | It extends CallToolResult to include information about the requested tool call. 29 | """ 30 | 31 | tool: str = Field(description="The name of the tool that was called.") 32 | arguments: dict[str, Any] = Field( 33 | description="The arguments used for the tool call." 34 | ) 35 | 36 | @classmethod 37 | def from_call_tool_result( 38 | cls, result: CallToolResult, tool: str, arguments: dict[str, Any] 39 | ) -> "CallToolRequestResult": 40 | """ 41 | Create a CallToolRequestResult from a CallToolResult. 42 | """ 43 | return cls( 44 | tool=tool, 45 | arguments=arguments, 46 | isError=result.isError, 47 | content=result.content, 48 | ) 49 | 50 | 51 | class BulkToolCaller(MCPMixin): 52 | """ 53 | A class to provide a "bulk tool call" tool for a FastMCP server 54 | """ 55 | 56 | def register_tools( 57 | self, 58 | mcp_server: "FastMCP", 59 | prefix: str | None = None, 60 | separator: str = _DEFAULT_SEPARATOR_TOOL, 61 | ) -> None: 62 | """ 63 | Register the tools provided by this class with the given MCP server. 64 | """ 65 | self.connection = FastMCPTransport(mcp_server) 66 | 67 | super().register_tools(mcp_server=mcp_server) 68 | 69 | @mcp_tool() 70 | async def call_tools_bulk( 71 | self, tool_calls: list[CallToolRequest], continue_on_error: bool = True 72 | ) -> list[CallToolRequestResult]: 73 | """ 74 | Call multiple tools registered on this MCP server in a single request. Each call can 75 | be for a different tool and can include different arguments. Useful for speeding up 76 | what would otherwise take several individual tool calls. 77 | """ 78 | results = [] 79 | 80 | for tool_call in tool_calls: 81 | result = await self._call_tool(tool_call.tool, tool_call.arguments) 82 | 83 | results.append(result) 84 | 85 | if result.isError and not continue_on_error: 86 | return results 87 | 88 | return results 89 | 90 | @mcp_tool() 91 | async def call_tool_bulk( 92 | self, 93 | tool: str, 94 | tool_arguments: list[dict[str, str | int | float | bool | None]], 95 | continue_on_error: bool = True, 96 | ) -> list[CallToolRequestResult]: 97 | """ 98 | Call a single tool registered on this MCP server multiple times with a single request. 99 | Each call can include different arguments. Useful for speeding up what would otherwise 100 | take several individual tool calls. 101 | 102 | Args: 103 | tool: The name of the tool to call. 104 | tool_arguments: A list of dictionaries, where each dictionary contains the arguments for an individual run of the tool. 105 | """ 106 | results = [] 107 | 108 | for tool_call_arguments in tool_arguments: 109 | result = await self._call_tool(tool, tool_call_arguments) 110 | 111 | results.append(result) 112 | 113 | if result.isError and not continue_on_error: 114 | return results 115 | 116 | return results 117 | 118 | async def _call_tool( 119 | self, tool: str, arguments: dict[str, Any] 120 | ) -> CallToolRequestResult: 121 | """ 122 | Helper method to call a tool with the provided arguments. 123 | """ 124 | 125 | async with Client(self.connection) as client: 126 | result = await client.call_tool_mcp(name=tool, arguments=arguments) 127 | 128 | return CallToolRequestResult( 129 | tool=tool, 130 | arguments=arguments, 131 | isError=result.isError, 132 | content=result.content, 133 | ) 134 | -------------------------------------------------------------------------------- /src/fastmcp/contrib/bulk_tool_caller/example.py: -------------------------------------------------------------------------------- 1 | """Sample code for FastMCP using MCPMixin.""" 2 | 3 | from fastmcp import FastMCP 4 | from fastmcp.contrib.bulk_tool_caller import BulkToolCaller 5 | 6 | mcp = FastMCP() 7 | 8 | 9 | @mcp.tool() 10 | def echo_tool(text: str) -> str: 11 | """Echo the input text""" 12 | return text 13 | 14 | 15 | bulk_tool_caller = BulkToolCaller() 16 | 17 | bulk_tool_caller.register_tools(mcp) 18 | -------------------------------------------------------------------------------- /src/fastmcp/contrib/mcp_mixin/README.md: -------------------------------------------------------------------------------- 1 | # MCP Mixin 2 | 3 | This module provides the `MCPMixin` base class and associated decorators (`@mcp_tool`, `@mcp_resource`, `@mcp_prompt`). 4 | 5 | It allows developers to easily define classes whose methods can be registered as tools, resources, or prompts with a `FastMCP` server instance using the `register_all()`, `register_tools()`, `register_resources()`, or `register_prompts()` methods provided by the mixin. 6 | 7 | ## Usage 8 | 9 | Inherit from `MCPMixin` and use the decorators on the methods you want to register. 10 | 11 | ```python 12 | from fastmcp import FastMCP 13 | from fastmcp.contrib.mcp_mixin import MCPMixin, mcp_tool, mcp_resource 14 | 15 | class MyComponent(MCPMixin): 16 | @mcp_tool(name="my_tool", description="Does something cool.") 17 | def tool_method(self): 18 | return "Tool executed!" 19 | 20 | @mcp_resource(uri="component://data") 21 | def resource_method(self): 22 | return {"data": "some data"} 23 | 24 | mcp_server = FastMCP() 25 | component = MyComponent() 26 | 27 | # Register all decorated methods with a prefix 28 | # Useful if you will have multiple instantiated objects of the same class 29 | # and want to avoid name collisions. 30 | component.register_all(mcp_server, prefix="my_comp") 31 | 32 | # Register without a prefix 33 | # component.register_all(mcp_server) 34 | 35 | # Now 'my_comp_my_tool' tool and 'my_comp+component://data' resource are registered (if prefix used) 36 | # Or 'my_tool' and 'component://data' are registered (if no prefix used) 37 | ``` 38 | 39 | The `prefix` argument in registration methods is optional. If omitted, methods are registered with their original decorated names/URIs. Individual separators (`tools_separator`, `resources_separator`, `prompts_separator`) can also be provided to `register_all` to change the separator for specific types. -------------------------------------------------------------------------------- /src/fastmcp/contrib/mcp_mixin/__init__.py: -------------------------------------------------------------------------------- 1 | from .mcp_mixin import MCPMixin, mcp_tool, mcp_resource, mcp_prompt 2 | 3 | __all__ = [ 4 | "MCPMixin", 5 | "mcp_tool", 6 | "mcp_resource", 7 | "mcp_prompt", 8 | ] 9 | -------------------------------------------------------------------------------- /src/fastmcp/contrib/mcp_mixin/example.py: -------------------------------------------------------------------------------- 1 | """Sample code for FastMCP using MCPMixin.""" 2 | 3 | import asyncio 4 | 5 | from fastmcp import FastMCP 6 | from fastmcp.contrib.mcp_mixin import ( 7 | MCPMixin, 8 | mcp_prompt, 9 | mcp_resource, 10 | mcp_tool, 11 | ) 12 | 13 | mcp = FastMCP() 14 | 15 | 16 | class Sample(MCPMixin): 17 | def __init__(self, name): 18 | self.name = name 19 | 20 | @mcp_tool() 21 | def first_tool(self): 22 | """First tool description.""" 23 | return f"Executed tool {self.name}." 24 | 25 | @mcp_resource(uri="test://test") 26 | def first_resource(self): 27 | """First resource description.""" 28 | return f"Executed resource {self.name}." 29 | 30 | @mcp_prompt() 31 | def first_prompt(self): 32 | """First prompt description.""" 33 | return f"here's a prompt! {self.name}." 34 | 35 | 36 | first_sample = Sample("First") 37 | second_sample = Sample("Second") 38 | 39 | first_sample.register_all(mcp_server=mcp, prefix="first") 40 | second_sample.register_all(mcp_server=mcp, prefix="second") 41 | 42 | 43 | async def list_components(): 44 | print("MCP Server running with registered components...") 45 | print("Tools:", list(await mcp.get_tools())) 46 | print("Resources:", list(await mcp.get_resources())) 47 | print("Prompts:", list(await mcp.get_prompts())) 48 | 49 | 50 | if __name__ == "__main__": 51 | asyncio.run(list_components()) 52 | mcp.run() 53 | -------------------------------------------------------------------------------- /src/fastmcp/exceptions.py: -------------------------------------------------------------------------------- 1 | """Custom exceptions for FastMCP.""" 2 | 3 | from mcp import McpError # noqa: F401 4 | 5 | 6 | class FastMCPError(Exception): 7 | """Base error for FastMCP.""" 8 | 9 | 10 | class ValidationError(FastMCPError): 11 | """Error in validating parameters or return values.""" 12 | 13 | 14 | class ResourceError(FastMCPError): 15 | """Error in resource operations.""" 16 | 17 | 18 | class ToolError(FastMCPError): 19 | """Error in tool operations.""" 20 | 21 | 22 | class PromptError(FastMCPError): 23 | """Error in prompt operations.""" 24 | 25 | 26 | class InvalidSignature(Exception): 27 | """Invalid signature for use with FastMCP.""" 28 | 29 | 30 | class ClientError(Exception): 31 | """Error in client operations.""" 32 | 33 | 34 | class NotFoundError(Exception): 35 | """Object not found.""" 36 | -------------------------------------------------------------------------------- /src/fastmcp/prompts/__init__.py: -------------------------------------------------------------------------------- 1 | from .prompt import Prompt, PromptMessage, Message 2 | from .prompt_manager import PromptManager 3 | 4 | __all__ = [ 5 | "Prompt", 6 | "PromptManager", 7 | "PromptMessage", 8 | "Message", 9 | ] 10 | -------------------------------------------------------------------------------- /src/fastmcp/prompts/prompt_manager.py: -------------------------------------------------------------------------------- 1 | """Prompt management functionality.""" 2 | 3 | from __future__ import annotations as _annotations 4 | 5 | from collections.abc import Awaitable, Callable 6 | from typing import TYPE_CHECKING, Any 7 | 8 | from mcp import GetPromptResult 9 | 10 | from fastmcp.exceptions import NotFoundError, PromptError 11 | from fastmcp.prompts.prompt import Prompt, PromptResult 12 | from fastmcp.settings import DuplicateBehavior 13 | from fastmcp.utilities.logging import get_logger 14 | 15 | if TYPE_CHECKING: 16 | pass 17 | 18 | logger = get_logger(__name__) 19 | 20 | 21 | class PromptManager: 22 | """Manages FastMCP prompts.""" 23 | 24 | def __init__( 25 | self, 26 | duplicate_behavior: DuplicateBehavior | None = None, 27 | mask_error_details: bool = False, 28 | ): 29 | self._prompts: dict[str, Prompt] = {} 30 | self.mask_error_details = mask_error_details 31 | 32 | # Default to "warn" if None is provided 33 | if duplicate_behavior is None: 34 | duplicate_behavior = "warn" 35 | 36 | if duplicate_behavior not in DuplicateBehavior.__args__: 37 | raise ValueError( 38 | f"Invalid duplicate_behavior: {duplicate_behavior}. " 39 | f"Must be one of: {', '.join(DuplicateBehavior.__args__)}" 40 | ) 41 | 42 | self.duplicate_behavior = duplicate_behavior 43 | 44 | def get_prompt(self, key: str) -> Prompt | None: 45 | """Get prompt by key.""" 46 | return self._prompts.get(key) 47 | 48 | def get_prompts(self) -> dict[str, Prompt]: 49 | """Get all registered prompts, indexed by registered key.""" 50 | return self._prompts 51 | 52 | def add_prompt_from_fn( 53 | self, 54 | fn: Callable[..., PromptResult | Awaitable[PromptResult]], 55 | name: str | None = None, 56 | description: str | None = None, 57 | tags: set[str] | None = None, 58 | ) -> Prompt: 59 | """Create a prompt from a function.""" 60 | prompt = Prompt.from_function(fn, name=name, description=description, tags=tags) 61 | return self.add_prompt(prompt) 62 | 63 | def add_prompt(self, prompt: Prompt, key: str | None = None) -> Prompt: 64 | """Add a prompt to the manager.""" 65 | key = key or prompt.name 66 | 67 | # Check for duplicates 68 | existing = self._prompts.get(key) 69 | if existing: 70 | if self.duplicate_behavior == "warn": 71 | logger.warning(f"Prompt already exists: {key}") 72 | self._prompts[key] = prompt 73 | elif self.duplicate_behavior == "replace": 74 | self._prompts[key] = prompt 75 | elif self.duplicate_behavior == "error": 76 | raise ValueError(f"Prompt already exists: {key}") 77 | elif self.duplicate_behavior == "ignore": 78 | return existing 79 | else: 80 | self._prompts[key] = prompt 81 | return prompt 82 | 83 | async def render_prompt( 84 | self, 85 | name: str, 86 | arguments: dict[str, Any] | None = None, 87 | ) -> GetPromptResult: 88 | """Render a prompt by name with arguments.""" 89 | prompt = self.get_prompt(name) 90 | if not prompt: 91 | raise NotFoundError(f"Unknown prompt: {name}") 92 | 93 | try: 94 | messages = await prompt.render(arguments) 95 | return GetPromptResult(description=prompt.description, messages=messages) 96 | 97 | # Pass through PromptErrors as-is 98 | except PromptError as e: 99 | logger.exception(f"Error rendering prompt {name!r}: {e}") 100 | raise e 101 | 102 | # Handle other exceptions 103 | except Exception as e: 104 | logger.exception(f"Error rendering prompt {name!r}: {e}") 105 | if self.mask_error_details: 106 | # Mask internal details 107 | raise PromptError(f"Error rendering prompt {name!r}") 108 | else: 109 | # Include original error details 110 | raise PromptError(f"Error rendering prompt {name!r}: {e}") 111 | 112 | def has_prompt(self, key: str) -> bool: 113 | """Check if a prompt exists.""" 114 | return key in self._prompts 115 | -------------------------------------------------------------------------------- /src/fastmcp/resources/__init__.py: -------------------------------------------------------------------------------- 1 | from .resource import Resource 2 | from .template import ResourceTemplate 3 | from .types import ( 4 | BinaryResource, 5 | DirectoryResource, 6 | FileResource, 7 | FunctionResource, 8 | HttpResource, 9 | TextResource, 10 | ) 11 | from .resource_manager import ResourceManager 12 | 13 | __all__ = [ 14 | "Resource", 15 | "TextResource", 16 | "BinaryResource", 17 | "FunctionResource", 18 | "FileResource", 19 | "HttpResource", 20 | "DirectoryResource", 21 | "ResourceTemplate", 22 | "ResourceManager", 23 | ] 24 | -------------------------------------------------------------------------------- /src/fastmcp/resources/resource.py: -------------------------------------------------------------------------------- 1 | """Base classes and interfaces for FastMCP resources.""" 2 | 3 | from __future__ import annotations 4 | 5 | import abc 6 | from typing import TYPE_CHECKING, Annotated, Any 7 | 8 | from mcp.types import Resource as MCPResource 9 | from pydantic import ( 10 | AnyUrl, 11 | BaseModel, 12 | BeforeValidator, 13 | ConfigDict, 14 | Field, 15 | UrlConstraints, 16 | ValidationInfo, 17 | field_validator, 18 | ) 19 | 20 | from fastmcp.utilities.types import _convert_set_defaults 21 | 22 | if TYPE_CHECKING: 23 | pass 24 | 25 | 26 | class Resource(BaseModel, abc.ABC): 27 | """Base class for all resources.""" 28 | 29 | model_config = ConfigDict(validate_default=True) 30 | 31 | uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] = Field( 32 | default=..., description="URI of the resource" 33 | ) 34 | name: str | None = Field(description="Name of the resource", default=None) 35 | description: str | None = Field( 36 | description="Description of the resource", default=None 37 | ) 38 | tags: Annotated[set[str], BeforeValidator(_convert_set_defaults)] = Field( 39 | default_factory=set, description="Tags for the resource" 40 | ) 41 | mime_type: str = Field( 42 | default="text/plain", 43 | description="MIME type of the resource content", 44 | pattern=r"^[a-zA-Z0-9]+/[a-zA-Z0-9\-+.]+$", 45 | ) 46 | 47 | @field_validator("mime_type", mode="before") 48 | @classmethod 49 | def set_default_mime_type(cls, mime_type: str | None) -> str: 50 | """Set default MIME type if not provided.""" 51 | if mime_type: 52 | return mime_type 53 | return "text/plain" 54 | 55 | @field_validator("name", mode="before") 56 | @classmethod 57 | def set_default_name(cls, name: str | None, info: ValidationInfo) -> str: 58 | """Set default name from URI if not provided.""" 59 | if name: 60 | return name 61 | if uri := info.data.get("uri"): 62 | return str(uri) 63 | raise ValueError("Either name or uri must be provided") 64 | 65 | @abc.abstractmethod 66 | async def read(self) -> str | bytes: 67 | """Read the resource content.""" 68 | pass 69 | 70 | def __eq__(self, other: object) -> bool: 71 | if not isinstance(other, Resource): 72 | return False 73 | return self.model_dump() == other.model_dump() 74 | 75 | def to_mcp_resource(self, **overrides: Any) -> MCPResource: 76 | """Convert the resource to an MCPResource.""" 77 | kwargs = { 78 | "uri": self.uri, 79 | "name": self.name, 80 | "description": self.description, 81 | "mimeType": self.mime_type, 82 | } 83 | return MCPResource(**kwargs | overrides) 84 | -------------------------------------------------------------------------------- /src/fastmcp/server/__init__.py: -------------------------------------------------------------------------------- 1 | from .server import FastMCP 2 | from .context import Context 3 | from . import dependencies 4 | 5 | 6 | __all__ = ["FastMCP", "Context"] 7 | -------------------------------------------------------------------------------- /src/fastmcp/server/auth/__init__.py: -------------------------------------------------------------------------------- 1 | from .providers.bearer import BearerAuthProvider 2 | 3 | 4 | __all__ = ["BearerAuthProvider"] 5 | -------------------------------------------------------------------------------- /src/fastmcp/server/auth/auth.py: -------------------------------------------------------------------------------- 1 | from mcp.server.auth.provider import ( 2 | AccessToken, 3 | AuthorizationCode, 4 | OAuthAuthorizationServerProvider, 5 | RefreshToken, 6 | ) 7 | from mcp.server.auth.settings import ( 8 | ClientRegistrationOptions, 9 | RevocationOptions, 10 | ) 11 | from pydantic import AnyHttpUrl 12 | 13 | 14 | class OAuthProvider( 15 | OAuthAuthorizationServerProvider[AuthorizationCode, RefreshToken, AccessToken] 16 | ): 17 | def __init__( 18 | self, 19 | issuer_url: AnyHttpUrl | str, 20 | service_documentation_url: AnyHttpUrl | str | None = None, 21 | client_registration_options: ClientRegistrationOptions | None = None, 22 | revocation_options: RevocationOptions | None = None, 23 | required_scopes: list[str] | None = None, 24 | ): 25 | """ 26 | Initialize the OAuth provider. 27 | 28 | Args: 29 | issuer_url: The URL of the OAuth issuer. 30 | service_documentation_url: The URL of the service documentation. 31 | client_registration_options: The client registration options. 32 | revocation_options: The revocation options. 33 | required_scopes: Scopes that are required for all requests. 34 | """ 35 | super().__init__() 36 | if isinstance(issuer_url, str): 37 | issuer_url = AnyHttpUrl(issuer_url) 38 | if isinstance(service_documentation_url, str): 39 | service_documentation_url = AnyHttpUrl(service_documentation_url) 40 | 41 | self.issuer_url = issuer_url 42 | self.service_documentation_url = service_documentation_url 43 | self.client_registration_options = client_registration_options 44 | self.revocation_options = revocation_options 45 | self.required_scopes = required_scopes 46 | -------------------------------------------------------------------------------- /src/fastmcp/server/auth/providers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlowin/fastmcp/97ef80c62478d0f366d62baf3e15875937cd7b9f/src/fastmcp/server/auth/providers/__init__.py -------------------------------------------------------------------------------- /src/fastmcp/server/auth/providers/bearer_env.py: -------------------------------------------------------------------------------- 1 | from pydantic_settings import BaseSettings, SettingsConfigDict 2 | 3 | from fastmcp.server.auth.providers.bearer import BearerAuthProvider 4 | 5 | 6 | # Sentinel object to indicate that a setting is not set 7 | class _NotSet: 8 | pass 9 | 10 | 11 | class EnvBearerAuthProviderSettings(BaseSettings): 12 | """Settings for the BearerAuthProvider.""" 13 | 14 | model_config = SettingsConfigDict( 15 | env_prefix="FASTMCP_AUTH_BEARER_", 16 | env_file=".env", 17 | extra="ignore", 18 | ) 19 | 20 | public_key: str | None = None 21 | jwks_uri: str | None = None 22 | issuer: str | None = None 23 | audience: str | None = None 24 | required_scopes: list[str] | None = None 25 | 26 | 27 | class EnvBearerAuthProvider(BearerAuthProvider): 28 | """ 29 | A BearerAuthProvider that loads settings from environment variables. Any 30 | providing setting will always take precedence over the environment 31 | variables. 32 | """ 33 | 34 | def __init__( 35 | self, 36 | public_key: str | None | type[_NotSet] = _NotSet, 37 | jwks_uri: str | None | type[_NotSet] = _NotSet, 38 | issuer: str | None | type[_NotSet] = _NotSet, 39 | audience: str | None | type[_NotSet] = _NotSet, 40 | required_scopes: list[str] | None | type[_NotSet] = _NotSet, 41 | ): 42 | """ 43 | Initialize the provider. 44 | 45 | Args: 46 | public_key: RSA public key in PEM format (for static key) 47 | jwks_uri: URI to fetch keys from (for key rotation) 48 | issuer: Expected issuer claim (optional) 49 | audience: Expected audience claim (optional) 50 | required_scopes: List of required scopes for access (optional) 51 | """ 52 | kwargs = { 53 | "public_key": public_key, 54 | "jwks_uri": jwks_uri, 55 | "issuer": issuer, 56 | "audience": audience, 57 | "required_scopes": required_scopes, 58 | } 59 | settings = EnvBearerAuthProviderSettings( 60 | **{k: v for k, v in kwargs.items() if v is not _NotSet} 61 | ) 62 | super().__init__(**settings.model_dump()) 63 | -------------------------------------------------------------------------------- /src/fastmcp/server/dependencies.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, ParamSpec, TypeVar 4 | 5 | from mcp.server.auth.middleware.auth_context import get_access_token 6 | from mcp.server.auth.provider import AccessToken 7 | from starlette.requests import Request 8 | 9 | if TYPE_CHECKING: 10 | from fastmcp.server.context import Context 11 | 12 | P = ParamSpec("P") 13 | R = TypeVar("R") 14 | 15 | __all__ = [ 16 | "get_context", 17 | "get_http_request", 18 | "get_http_headers", 19 | "get_access_token", 20 | "AccessToken", 21 | ] 22 | 23 | 24 | # --- Context --- 25 | 26 | 27 | def get_context() -> Context: 28 | from fastmcp.server.context import _current_context 29 | 30 | context = _current_context.get() 31 | if context is None: 32 | raise RuntimeError("No active context found.") 33 | return context 34 | 35 | 36 | # --- HTTP Request --- 37 | 38 | 39 | def get_http_request() -> Request: 40 | from fastmcp.server.http import _current_http_request 41 | 42 | request = _current_http_request.get() 43 | if request is None: 44 | raise RuntimeError("No active HTTP request found.") 45 | return request 46 | 47 | 48 | def get_http_headers(include_all: bool = False) -> dict[str, str]: 49 | """ 50 | Extract headers from the current HTTP request if available. 51 | 52 | Never raises an exception, even if there is no active HTTP request (in which case 53 | an empty dict is returned). 54 | 55 | By default, strips problematic headers like `content-length` that cause issues if forwarded to downstream clients. 56 | If `include_all` is True, all headers are returned. 57 | """ 58 | if include_all: 59 | exclude_headers = set() 60 | else: 61 | exclude_headers = { 62 | "host", 63 | "content-length", 64 | "connection", 65 | "transfer-encoding", 66 | "upgrade", 67 | "te", 68 | "keep-alive", 69 | "expect", 70 | # Proxy-related headers 71 | "proxy-authenticate", 72 | "proxy-authorization", 73 | "proxy-connection", 74 | } 75 | # (just in case) 76 | if not all(h.lower() == h for h in exclude_headers): 77 | raise ValueError("Excluded headers must be lowercase") 78 | headers = {} 79 | 80 | try: 81 | request = get_http_request() 82 | for name, value in request.headers.items(): 83 | lower_name = name.lower() 84 | if lower_name not in exclude_headers: 85 | headers[lower_name] = str(value) 86 | return headers 87 | except RuntimeError: 88 | return {} 89 | -------------------------------------------------------------------------------- /src/fastmcp/tools/__init__.py: -------------------------------------------------------------------------------- 1 | from .tool import Tool 2 | from .tool_manager import ToolManager 3 | 4 | __all__ = ["Tool", "ToolManager"] 5 | -------------------------------------------------------------------------------- /src/fastmcp/utilities/__init__.py: -------------------------------------------------------------------------------- 1 | """FastMCP utility modules.""" 2 | -------------------------------------------------------------------------------- /src/fastmcp/utilities/cache.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import Any 3 | 4 | UTC = datetime.timezone.utc 5 | 6 | 7 | class TimedCache: 8 | NOT_FOUND = object() 9 | 10 | def __init__(self, expiration: datetime.timedelta): 11 | self.expiration = expiration 12 | self.cache: dict[Any, tuple[Any, datetime.datetime]] = {} 13 | 14 | def set(self, key: Any, value: Any) -> None: 15 | expires = datetime.datetime.now(UTC) + self.expiration 16 | self.cache[key] = (value, expires) 17 | 18 | def get(self, key: Any) -> Any: 19 | value = self.cache.get(key) 20 | if value is not None and value[1] > datetime.datetime.now(UTC): 21 | return value[0] 22 | else: 23 | return self.NOT_FOUND 24 | 25 | def clear(self) -> None: 26 | self.cache.clear() 27 | -------------------------------------------------------------------------------- /src/fastmcp/utilities/decorators.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from collections.abc import Callable 3 | from typing import Generic, ParamSpec, TypeVar, cast, overload 4 | 5 | from typing_extensions import Self 6 | 7 | R = TypeVar("R") 8 | P = ParamSpec("P") 9 | 10 | 11 | class DecoratedFunction(Generic[P, R]): 12 | """Descriptor for decorated functions. 13 | 14 | You can return this object from a decorator to ensure that it works across 15 | all types of functions: vanilla, instance methods, class methods, and static 16 | methods; both synchronous and asynchronous. 17 | 18 | This class is used to store the original function and metadata about how to 19 | register it as a tool. 20 | 21 | Example usage: 22 | 23 | ```python 24 | def my_decorator(fn: Callable[P, R]) -> DecoratedFunction[P, R]: 25 | return DecoratedFunction(fn) 26 | ``` 27 | 28 | On a function: 29 | ```python 30 | @my_decorator 31 | def my_function(a: int, b: int) -> int: 32 | return a + b 33 | ``` 34 | 35 | On an instance method: 36 | ```python 37 | class Test: 38 | @my_decorator 39 | def my_function(self, a: int, b: int) -> int: 40 | return a + b 41 | ``` 42 | 43 | On a class method: 44 | ```python 45 | class Test: 46 | @classmethod 47 | @my_decorator 48 | def my_function(cls, a: int, b: int) -> int: 49 | return a + b 50 | ``` 51 | 52 | Note that for classmethods, the decorator must be applied first, then 53 | `@classmethod` on top. 54 | 55 | On a static method: 56 | ```python 57 | class Test: 58 | @staticmethod 59 | @my_decorator 60 | def my_function(a: int, b: int) -> int: 61 | return a + b 62 | ``` 63 | """ 64 | 65 | def __init__(self, fn: Callable[P, R]): 66 | self.fn = fn 67 | 68 | def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R: 69 | """Call the original function.""" 70 | try: 71 | return self.fn(*args, **kwargs) 72 | except TypeError as e: 73 | if "'classmethod' object is not callable" in str(e): 74 | raise TypeError( 75 | "To apply this decorator to a classmethod, apply the decorator first, then @classmethod on top." 76 | ) 77 | raise 78 | 79 | @overload 80 | def __get__(self, instance: None, owner: type | None = None) -> Self: ... 81 | 82 | @overload 83 | def __get__( 84 | self, instance: object, owner: type | None = None 85 | ) -> Callable[P, R]: ... 86 | 87 | def __get__( 88 | self, instance: object | None, owner: type | None = None 89 | ) -> Self | Callable[P, R]: 90 | """Return the original function when accessed from an instance, or self when accessed from the class.""" 91 | if instance is None: 92 | return self 93 | # Return the original function bound to the instance 94 | return cast(Callable[P, R], self.fn.__get__(instance, owner)) 95 | 96 | def __repr__(self) -> str: 97 | """Return a representation that matches Python's function representation.""" 98 | module = getattr(self.fn, "__module__", "unknown") 99 | qualname = getattr(self.fn, "__qualname__", str(self.fn)) 100 | sig_str = str(inspect.signature(self.fn)) 101 | return f"" 102 | -------------------------------------------------------------------------------- /src/fastmcp/utilities/exceptions.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable, Iterable, Mapping 2 | from typing import Any 3 | 4 | import httpx 5 | import mcp.types 6 | from exceptiongroup import BaseExceptionGroup 7 | from mcp import McpError 8 | 9 | import fastmcp 10 | 11 | 12 | def iter_exc(group: BaseExceptionGroup): 13 | for exc in group.exceptions: 14 | if isinstance(exc, BaseExceptionGroup): 15 | yield from iter_exc(exc) 16 | else: 17 | yield exc 18 | 19 | 20 | def _exception_handler(group: BaseExceptionGroup): 21 | for leaf in iter_exc(group): 22 | if isinstance(leaf, httpx.ConnectTimeout): 23 | raise McpError( 24 | error=mcp.types.ErrorData( 25 | code=httpx.codes.REQUEST_TIMEOUT, 26 | message="Timed out while waiting for response.", 27 | ) 28 | ) 29 | raise leaf 30 | 31 | 32 | # this catch handler is used to catch taskgroup exception groups and raise the 33 | # first exception. This allows more sane debugging. 34 | _catch_handlers: Mapping[ 35 | type[BaseException] | Iterable[type[BaseException]], 36 | Callable[[BaseExceptionGroup[Any]], Any], 37 | ] = { 38 | Exception: _exception_handler, 39 | } 40 | 41 | 42 | def get_catch_handlers() -> Mapping[ 43 | type[BaseException] | Iterable[type[BaseException]], 44 | Callable[[BaseExceptionGroup[Any]], Any], 45 | ]: 46 | if fastmcp.settings.settings.client_raise_first_exceptiongroup_error: 47 | return _catch_handlers 48 | else: 49 | return {} 50 | -------------------------------------------------------------------------------- /src/fastmcp/utilities/http.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | 4 | def find_available_port() -> int: 5 | """Find an available port by letting the OS assign one.""" 6 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: 7 | s.bind(("127.0.0.1", 0)) 8 | return s.getsockname()[1] 9 | -------------------------------------------------------------------------------- /src/fastmcp/utilities/json_schema.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import copy 4 | 5 | 6 | def _prune_param(schema: dict, param: str) -> dict: 7 | """Return a new schema with *param* removed from `properties`, `required`, 8 | and (if no longer referenced) `$defs`. 9 | """ 10 | 11 | # ── 1. drop from properties/required ────────────────────────────── 12 | props = schema.get("properties", {}) 13 | removed = props.pop(param, None) 14 | if removed is None: # nothing to do 15 | return schema 16 | 17 | # Keep empty properties object rather than removing it entirely 18 | schema["properties"] = props 19 | if param in schema.get("required", []): 20 | schema["required"].remove(param) 21 | if not schema["required"]: 22 | schema.pop("required") 23 | 24 | return schema 25 | 26 | 27 | def _walk_and_prune( 28 | schema: dict, 29 | prune_defs: bool = False, 30 | prune_titles: bool = False, 31 | prune_additional_properties: bool = False, 32 | ) -> dict: 33 | """Walk the schema and optionally prune titles, unused definitions, and additionalProperties: false.""" 34 | 35 | # Will only be used if prune_defs is True 36 | used_defs: set[str] = set() 37 | 38 | def walk(node: object) -> None: 39 | if isinstance(node, dict): 40 | # Process $ref for definition tracking 41 | if prune_defs: 42 | ref = node.get("$ref") 43 | if isinstance(ref, str) and ref.startswith("#/$defs/"): 44 | used_defs.add(ref.split("/")[-1]) 45 | 46 | # Remove title if requested 47 | if prune_titles and "title" in node: 48 | node.pop("title") 49 | 50 | # Remove additionalProperties: false at any level if requested 51 | if ( 52 | prune_additional_properties 53 | and node.get("additionalProperties", None) is False 54 | ): 55 | node.pop("additionalProperties") 56 | 57 | # Walk children 58 | for v in node.values(): 59 | walk(v) 60 | 61 | elif isinstance(node, list): 62 | for v in node: 63 | walk(v) 64 | 65 | # Traverse the schema once 66 | walk(schema) 67 | 68 | # Remove orphaned definitions if requested 69 | if prune_defs: 70 | defs = schema.get("$defs", {}) 71 | for def_name in list(defs): 72 | if def_name not in used_defs: 73 | defs.pop(def_name) 74 | if not defs: 75 | schema.pop("$defs", None) 76 | 77 | return schema 78 | 79 | 80 | def _prune_additional_properties(schema: dict) -> dict: 81 | """Remove additionalProperties from the schema if it is False.""" 82 | if schema.get("additionalProperties", None) is False: 83 | schema.pop("additionalProperties") 84 | return schema 85 | 86 | 87 | def compress_schema( 88 | schema: dict, 89 | prune_params: list[str] | None = None, 90 | prune_defs: bool = True, 91 | prune_additional_properties: bool = True, 92 | prune_titles: bool = False, 93 | ) -> dict: 94 | """ 95 | Remove the given parameters from the schema. 96 | 97 | Args: 98 | schema: The schema to compress 99 | prune_params: List of parameter names to remove from properties 100 | prune_defs: Whether to remove unused definitions 101 | prune_additional_properties: Whether to remove additionalProperties: false 102 | prune_titles: Whether to remove title fields from the schema 103 | """ 104 | # Make a copy so we don't modify the original 105 | schema = copy.deepcopy(schema) 106 | 107 | # Remove specific parameters if requested 108 | for param in prune_params or []: 109 | schema = _prune_param(schema, param=param) 110 | 111 | # Do a single walk to handle pruning operations 112 | if prune_defs or prune_titles or prune_additional_properties: 113 | schema = _walk_and_prune( 114 | schema, 115 | prune_defs=prune_defs, 116 | prune_titles=prune_titles, 117 | prune_additional_properties=prune_additional_properties, 118 | ) 119 | 120 | return schema 121 | -------------------------------------------------------------------------------- /src/fastmcp/utilities/logging.py: -------------------------------------------------------------------------------- 1 | """Logging utilities for FastMCP.""" 2 | 3 | import logging 4 | from typing import Literal 5 | 6 | from rich.console import Console 7 | from rich.logging import RichHandler 8 | 9 | 10 | def get_logger(name: str) -> logging.Logger: 11 | """Get a logger nested under FastMCP namespace. 12 | 13 | Args: 14 | name: the name of the logger, which will be prefixed with 'FastMCP.' 15 | 16 | Returns: 17 | a configured logger instance 18 | """ 19 | return logging.getLogger(f"FastMCP.{name}") 20 | 21 | 22 | def configure_logging( 23 | level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | int = "INFO", 24 | logger: logging.Logger | None = None, 25 | enable_rich_tracebacks: bool = True, 26 | ) -> None: 27 | """ 28 | Configure logging for FastMCP. 29 | 30 | Args: 31 | logger: the logger to configure 32 | level: the log level to use 33 | """ 34 | 35 | if logger is None: 36 | logger = logging.getLogger("FastMCP") 37 | 38 | # Only configure the FastMCP logger namespace 39 | handler = RichHandler( 40 | console=Console(stderr=True), 41 | rich_tracebacks=enable_rich_tracebacks, 42 | ) 43 | formatter = logging.Formatter("%(message)s") 44 | handler.setFormatter(formatter) 45 | 46 | logger.setLevel(level) 47 | 48 | # Remove any existing handlers to avoid duplicates on reconfiguration 49 | for hdlr in logger.handlers[:]: 50 | logger.removeHandler(hdlr) 51 | 52 | logger.addHandler(handler) 53 | -------------------------------------------------------------------------------- /src/fastmcp/utilities/mcp_config.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Any, Literal 4 | from urllib.parse import urlparse 5 | 6 | from pydantic import AnyUrl, BaseModel, Field 7 | 8 | if TYPE_CHECKING: 9 | from fastmcp.client.transports import ( 10 | SSETransport, 11 | StdioTransport, 12 | StreamableHttpTransport, 13 | ) 14 | 15 | 16 | def infer_transport_type_from_url( 17 | url: str | AnyUrl, 18 | ) -> Literal["streamable-http", "sse"]: 19 | """ 20 | Infer the appropriate transport type from the given URL. 21 | """ 22 | url = str(url) 23 | if not url.startswith("http"): 24 | raise ValueError(f"Invalid URL: {url}") 25 | 26 | parsed_url = urlparse(url) 27 | path = parsed_url.path 28 | 29 | if "/sse/" in path or path.rstrip("/").endswith("/sse"): 30 | return "sse" 31 | else: 32 | return "streamable-http" 33 | 34 | 35 | class StdioMCPServer(BaseModel): 36 | command: str 37 | args: list[str] = Field(default_factory=list) 38 | env: dict[str, Any] = Field(default_factory=dict) 39 | cwd: str | None = None 40 | transport: Literal["stdio"] = "stdio" 41 | 42 | def to_transport(self) -> StdioTransport: 43 | from fastmcp.client.transports import StdioTransport 44 | 45 | return StdioTransport( 46 | command=self.command, 47 | args=self.args, 48 | env=self.env, 49 | cwd=self.cwd, 50 | ) 51 | 52 | 53 | class RemoteMCPServer(BaseModel): 54 | url: str 55 | headers: dict[str, str] = Field(default_factory=dict) 56 | transport: Literal["streamable-http", "sse", "http"] | None = None 57 | 58 | def to_transport(self) -> StreamableHttpTransport | SSETransport: 59 | from fastmcp.client.transports import SSETransport, StreamableHttpTransport 60 | 61 | if self.transport is None: 62 | transport = infer_transport_type_from_url(self.url) 63 | else: 64 | transport = self.transport 65 | 66 | if transport == "sse": 67 | return SSETransport(self.url, headers=self.headers) 68 | else: 69 | return StreamableHttpTransport(self.url, headers=self.headers) 70 | 71 | 72 | class MCPConfig(BaseModel): 73 | mcpServers: dict[str, StdioMCPServer | RemoteMCPServer] 74 | 75 | @classmethod 76 | def from_dict(cls, config: dict[str, Any]) -> MCPConfig: 77 | return cls(mcpServers=config.get("mcpServers", config)) 78 | -------------------------------------------------------------------------------- /src/fastmcp/utilities/tests.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import copy 4 | import multiprocessing 5 | import socket 6 | import time 7 | from collections.abc import Callable, Generator 8 | from contextlib import contextmanager 9 | from typing import TYPE_CHECKING, Any, Literal 10 | 11 | import uvicorn 12 | 13 | from fastmcp.settings import settings 14 | from fastmcp.utilities.http import find_available_port 15 | 16 | if TYPE_CHECKING: 17 | from fastmcp.server.server import FastMCP 18 | 19 | 20 | @contextmanager 21 | def temporary_settings(**kwargs: Any): 22 | """ 23 | Temporarily override ControlFlow setting values. 24 | 25 | Args: 26 | **kwargs: The settings to override, including nested settings. 27 | 28 | Example: 29 | Temporarily override a setting: 30 | ```python 31 | import fastmcp 32 | from fastmcp.utilities.tests import temporary_settings 33 | 34 | with temporary_settings(log_level='DEBUG'): 35 | assert fastmcp.settings.settings.log_level == 'DEBUG' 36 | assert fastmcp.settings.settings.log_level == 'INFO' 37 | ``` 38 | """ 39 | old_settings = copy.deepcopy(settings.model_dump()) 40 | 41 | try: 42 | # apply the new settings 43 | for attr, value in kwargs.items(): 44 | if not hasattr(settings, attr): 45 | raise AttributeError(f"Setting {attr} does not exist.") 46 | setattr(settings, attr, value) 47 | yield 48 | 49 | finally: 50 | # restore the old settings 51 | for attr in kwargs: 52 | if hasattr(settings, attr): 53 | setattr(settings, attr, old_settings[attr]) 54 | 55 | 56 | def _run_server(mcp_server: FastMCP, transport: Literal["sse"], port: int) -> None: 57 | # Some Starlette apps are not pickleable, so we need to create them here based on the indicated transport 58 | if transport == "sse": 59 | app = mcp_server.http_app(transport="sse") 60 | else: 61 | raise ValueError(f"Invalid transport: {transport}") 62 | uvicorn_server = uvicorn.Server( 63 | config=uvicorn.Config( 64 | app=app, 65 | host="127.0.0.1", 66 | port=port, 67 | log_level="error", 68 | ) 69 | ) 70 | uvicorn_server.run() 71 | 72 | 73 | @contextmanager 74 | def run_server_in_process( 75 | server_fn: Callable[..., None], 76 | *args, 77 | provide_host_and_port: bool = True, 78 | **kwargs, 79 | ) -> Generator[str, None, None]: 80 | """ 81 | Context manager that runs a FastMCP server in a separate process and 82 | returns the server URL. When the context manager is exited, the server process is killed. 83 | 84 | Args: 85 | server_fn: The function that runs a FastMCP server. FastMCP servers are 86 | not pickleable, so we need a function that creates and runs one. 87 | *args: Arguments to pass to the server function. 88 | provide_host_and_port: Whether to provide the host and port to the server function as kwargs. 89 | **kwargs: Keyword arguments to pass to the server function. 90 | 91 | Returns: 92 | The server URL. 93 | """ 94 | host = "127.0.0.1" 95 | port = find_available_port() 96 | 97 | if provide_host_and_port: 98 | kwargs |= {"host": host, "port": port} 99 | 100 | proc = multiprocessing.Process( 101 | target=server_fn, args=args, kwargs=kwargs, daemon=True 102 | ) 103 | proc.start() 104 | 105 | # Wait for server to be running 106 | max_attempts = 10 107 | attempt = 0 108 | while attempt < max_attempts and proc.is_alive(): 109 | try: 110 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: 111 | s.connect((host, port)) 112 | break 113 | except ConnectionRefusedError: 114 | if attempt < 3: 115 | time.sleep(0.01) 116 | else: 117 | time.sleep(0.1) 118 | attempt += 1 119 | else: 120 | raise RuntimeError(f"Server failed to start after {max_attempts} attempts") 121 | 122 | yield f"http://{host}:{port}" 123 | 124 | proc.terminate() 125 | proc.join(timeout=5) 126 | if proc.is_alive(): 127 | # If it's still alive, then force kill it 128 | proc.kill() 129 | proc.join(timeout=2) 130 | if proc.is_alive(): 131 | raise RuntimeError("Server process failed to terminate even after kill") 132 | -------------------------------------------------------------------------------- /src/fastmcp/utilities/types.py: -------------------------------------------------------------------------------- 1 | """Common types used across FastMCP.""" 2 | 3 | import base64 4 | import inspect 5 | from collections.abc import Callable 6 | from functools import lru_cache 7 | from pathlib import Path 8 | from types import UnionType 9 | from typing import Annotated, TypeVar, Union, get_args, get_origin 10 | 11 | from mcp.types import ImageContent 12 | from pydantic import TypeAdapter 13 | 14 | T = TypeVar("T") 15 | 16 | 17 | @lru_cache(maxsize=5000) 18 | def get_cached_typeadapter(cls: T) -> TypeAdapter[T]: 19 | """ 20 | TypeAdapters are heavy objects, and in an application context we'd typically 21 | create them once in a global scope and reuse them as often as possible. 22 | However, this isn't feasible for user-generated functions. Instead, we use a 23 | cache to minimize the cost of creating them as much as possible. 24 | """ 25 | return TypeAdapter(cls) 26 | 27 | 28 | def issubclass_safe(cls: type, base: type) -> bool: 29 | """Check if cls is a subclass of base, even if cls is a type variable.""" 30 | try: 31 | if origin := get_origin(cls): 32 | return issubclass_safe(origin, base) 33 | return issubclass(cls, base) 34 | except TypeError: 35 | return False 36 | 37 | 38 | def is_class_member_of_type(cls: type, base: type) -> bool: 39 | """ 40 | Check if cls is a member of base, even if cls is a type variable. 41 | 42 | Base can be a type, a UnionType, or an Annotated type. Generic types are not 43 | considered members (e.g. T is not a member of list[T]). 44 | """ 45 | origin = get_origin(cls) 46 | # Handle both types of unions: UnionType (from types module, used with | syntax) 47 | # and typing.Union (used with Union[] syntax) 48 | if origin is UnionType or origin == Union: 49 | return any(is_class_member_of_type(arg, base) for arg in get_args(cls)) 50 | elif origin is Annotated: 51 | # For Annotated[T, ...], check if T is a member of base 52 | args = get_args(cls) 53 | if args: 54 | return is_class_member_of_type(args[0], base) 55 | return False 56 | else: 57 | return issubclass_safe(cls, base) 58 | 59 | 60 | def find_kwarg_by_type(fn: Callable, kwarg_type: type) -> str | None: 61 | """ 62 | Find the name of the kwarg that is of type kwarg_type. 63 | 64 | Includes union types that contain the kwarg_type, as well as Annotated types. 65 | """ 66 | if inspect.ismethod(fn) and hasattr(fn, "__func__"): 67 | sig = inspect.signature(fn.__func__) 68 | else: 69 | sig = inspect.signature(fn) 70 | 71 | for name, param in sig.parameters.items(): 72 | if is_class_member_of_type(param.annotation, kwarg_type): 73 | return name 74 | return None 75 | 76 | 77 | def _convert_set_defaults(maybe_set: set[T] | list[T] | None) -> set[T]: 78 | """Convert a set or list to a set, defaulting to an empty set if None.""" 79 | if maybe_set is None: 80 | return set() 81 | if isinstance(maybe_set, set): 82 | return maybe_set 83 | return set(maybe_set) 84 | 85 | 86 | class Image: 87 | """Helper class for returning images from tools.""" 88 | 89 | def __init__( 90 | self, 91 | path: str | Path | None = None, 92 | data: bytes | None = None, 93 | format: str | None = None, 94 | ): 95 | if path is None and data is None: 96 | raise ValueError("Either path or data must be provided") 97 | if path is not None and data is not None: 98 | raise ValueError("Only one of path or data can be provided") 99 | 100 | self.path = Path(path) if path else None 101 | self.data = data 102 | self._format = format 103 | self._mime_type = self._get_mime_type() 104 | 105 | def _get_mime_type(self) -> str: 106 | """Get MIME type from format or guess from file extension.""" 107 | if self._format: 108 | return f"image/{self._format.lower()}" 109 | 110 | if self.path: 111 | suffix = self.path.suffix.lower() 112 | return { 113 | ".png": "image/png", 114 | ".jpg": "image/jpeg", 115 | ".jpeg": "image/jpeg", 116 | ".gif": "image/gif", 117 | ".webp": "image/webp", 118 | }.get(suffix, "application/octet-stream") 119 | return "image/png" # default for raw binary data 120 | 121 | def to_image_content(self) -> ImageContent: 122 | """Convert to MCP ImageContent.""" 123 | if self.path: 124 | with open(self.path, "rb") as f: 125 | data = base64.b64encode(f.read()).decode() 126 | elif self.data is not None: 127 | data = base64.b64encode(self.data).decode() 128 | else: 129 | raise ValueError("No image data available") 130 | 131 | return ImageContent(type="image", data=data, mimeType=self._mime_type) 132 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlowin/fastmcp/97ef80c62478d0f366d62baf3e15875937cd7b9f/tests/__init__.py -------------------------------------------------------------------------------- /tests/auth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlowin/fastmcp/97ef80c62478d0f366d62baf3e15875937cd7b9f/tests/auth/__init__.py -------------------------------------------------------------------------------- /tests/auth/providers/test_bearer_env.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pydantic import AnyHttpUrl, ValidationError 3 | 4 | from fastmcp import FastMCP 5 | from fastmcp.server.auth.providers.bearer import BearerAuthProvider 6 | from fastmcp.server.auth.providers.bearer_env import EnvBearerAuthProvider 7 | 8 | 9 | def test_load_bearer_env_from_env_var(monkeypatch): 10 | mcp = FastMCP() 11 | assert mcp.auth is None 12 | 13 | monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env") 14 | monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key") 15 | 16 | mcp_with_auth = FastMCP() 17 | assert isinstance(mcp_with_auth.auth, EnvBearerAuthProvider) 18 | 19 | 20 | def test_load_bearer_env_from_env_var_requires_public_key_or_jwks_uri(monkeypatch): 21 | mcp = FastMCP() 22 | assert mcp.auth is None 23 | 24 | monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env") 25 | 26 | with pytest.raises( 27 | ValueError, match="Either public_key or jwks_uri must be provided" 28 | ): 29 | FastMCP() 30 | 31 | 32 | def test_configure_bearer_env_from_env_var(monkeypatch): 33 | monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env") 34 | monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key") 35 | monkeypatch.setenv("FASTMCP_AUTH_BEARER_ISSUER", "http://test-issuer") 36 | monkeypatch.setenv("FASTMCP_AUTH_BEARER_AUDIENCE", "test-audience") 37 | monkeypatch.setenv( 38 | "FASTMCP_AUTH_BEARER_REQUIRED_SCOPES", '["test-scope1", "test-scope2"]' 39 | ) 40 | 41 | mcp = FastMCP() 42 | assert isinstance(mcp.auth, EnvBearerAuthProvider) 43 | assert mcp.auth.public_key == "test-public-key" 44 | assert mcp.auth.issuer_url == AnyHttpUrl("http://test-issuer") 45 | assert mcp.auth.audience == "test-audience" 46 | assert mcp.auth.required_scopes == ["test-scope1", "test-scope2"] 47 | 48 | 49 | def test_list_of_scopes_must_be_a_list(monkeypatch): 50 | monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env") 51 | monkeypatch.setenv("FASTMCP_AUTH_BEARER_REQUIRED_SCOPES", "test-scope1") 52 | 53 | with pytest.raises(ValidationError, match="Input should be a valid list"): 54 | FastMCP() 55 | 56 | 57 | def test_configure_bearer_env_jwks_uri_from_env_var(monkeypatch): 58 | monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env") 59 | monkeypatch.setenv("FASTMCP_AUTH_BEARER_JWKS_URI", "test-jwks-uri") 60 | 61 | mcp = FastMCP() 62 | assert isinstance(mcp.auth, EnvBearerAuthProvider) 63 | assert mcp.auth.jwks_uri == "test-jwks-uri" 64 | 65 | 66 | def test_configure_bearer_env_public_key_and_jwks_uri_error(monkeypatch): 67 | monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env") 68 | monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key") 69 | monkeypatch.setenv("FASTMCP_AUTH_BEARER_JWKS_URI", "test-jwks-uri") 70 | 71 | with pytest.raises(ValueError, match="Provide either public_key or jwks_uri"): 72 | FastMCP() 73 | 74 | 75 | def test_provided_auth_takes_precedence_over_env_vars(monkeypatch): 76 | monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env") 77 | monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key") 78 | 79 | mcp = FastMCP(auth=BearerAuthProvider(public_key="test-public-key-2")) 80 | assert isinstance(mcp.auth, BearerAuthProvider) 81 | assert not isinstance(mcp.auth, EnvBearerAuthProvider) 82 | assert mcp.auth.public_key == "test-public-key-2" 83 | -------------------------------------------------------------------------------- /tests/cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlowin/fastmcp/97ef80c62478d0f366d62baf3e15875937cd7b9f/tests/cli/__init__.py -------------------------------------------------------------------------------- /tests/client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlowin/fastmcp/97ef80c62478d0f366d62baf3e15875937cd7b9f/tests/client/__init__.py -------------------------------------------------------------------------------- /tests/client/test_logs.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from mcp import LoggingLevel 3 | 4 | from fastmcp import Client, Context, FastMCP 5 | from fastmcp.client.logging import LogMessage 6 | 7 | 8 | class LogHandler: 9 | def __init__(self): 10 | self.logs: list[LogMessage] = [] 11 | 12 | async def handle_log(self, message: LogMessage) -> None: 13 | self.logs.append(message) 14 | 15 | 16 | @pytest.fixture 17 | def fastmcp_server(): 18 | mcp = FastMCP() 19 | 20 | @mcp.tool() 21 | async def log(context: Context) -> None: 22 | await context.info(message="hello?") 23 | 24 | @mcp.tool() 25 | async def echo_log( 26 | message: str, 27 | context: Context, 28 | level: LoggingLevel | None = None, 29 | logger: str | None = None, 30 | ) -> None: 31 | await context.log(message=message, level=level) 32 | 33 | return mcp 34 | 35 | 36 | class TestClientLogs: 37 | async def test_log(self, fastmcp_server: FastMCP): 38 | log_handler = LogHandler() 39 | async with Client(fastmcp_server, log_handler=log_handler.handle_log) as client: 40 | await client.call_tool("log", {}) 41 | 42 | assert len(log_handler.logs) == 1 43 | assert log_handler.logs[0].data == "hello?" 44 | assert log_handler.logs[0].level == "info" 45 | 46 | async def test_echo_log(self, fastmcp_server: FastMCP): 47 | log_handler = LogHandler() 48 | async with Client(fastmcp_server, log_handler=log_handler.handle_log) as client: 49 | await client.call_tool("echo_log", {"message": "this is a log"}) 50 | 51 | assert len(log_handler.logs) == 1 52 | await client.call_tool( 53 | "echo_log", {"message": "this is a warning log", "level": "warning"} 54 | ) 55 | assert len(log_handler.logs) == 2 56 | 57 | assert log_handler.logs[0].data == "this is a log" 58 | assert log_handler.logs[0].level == "info" 59 | assert log_handler.logs[1].data == "this is a warning log" 60 | assert log_handler.logs[1].level == "warning" 61 | -------------------------------------------------------------------------------- /tests/client/test_progress.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from fastmcp import Client, Context, FastMCP 4 | 5 | PROGRESS_MESSAGES = [] 6 | 7 | 8 | @pytest.fixture(autouse=True) 9 | def clear_progress_messages(): 10 | PROGRESS_MESSAGES.clear() 11 | yield 12 | PROGRESS_MESSAGES.clear() 13 | 14 | 15 | @pytest.fixture 16 | def fastmcp_server(): 17 | mcp = FastMCP() 18 | 19 | @mcp.tool() 20 | async def progress_tool(context: Context) -> int: 21 | for i in range(3): 22 | await context.report_progress( 23 | progress=i + 1, 24 | total=3, 25 | message=f"{(i + 1) / 3 * 100:.2f}% complete", 26 | ) 27 | return 100 28 | 29 | return mcp 30 | 31 | 32 | EXPECTED_PROGRESS_MESSAGES = [ 33 | dict(progress=1, total=3, message="33.33% complete"), 34 | dict(progress=2, total=3, message="66.67% complete"), 35 | dict(progress=3, total=3, message="100.00% complete"), 36 | ] 37 | 38 | 39 | async def progress_handler( 40 | progress: float, total: float | None, message: str | None 41 | ) -> None: 42 | PROGRESS_MESSAGES.append(dict(progress=progress, total=total, message=message)) 43 | 44 | 45 | async def test_progress_handler(fastmcp_server: FastMCP): 46 | async with Client(fastmcp_server, progress_handler=progress_handler) as client: 47 | await client.call_tool("progress_tool", {}) 48 | 49 | assert PROGRESS_MESSAGES == EXPECTED_PROGRESS_MESSAGES 50 | 51 | 52 | async def test_progress_handler_can_be_supplied_on_tool_call(fastmcp_server: FastMCP): 53 | async with Client(fastmcp_server) as client: 54 | await client.call_tool("progress_tool", {}, progress_handler=progress_handler) 55 | 56 | assert PROGRESS_MESSAGES == EXPECTED_PROGRESS_MESSAGES 57 | 58 | 59 | async def test_progress_handler_supplied_on_tool_call_overrides_default( 60 | fastmcp_server: FastMCP, 61 | ): 62 | async def bad_progress_handler( 63 | progress: float, total: float | None, message: str | None 64 | ) -> None: 65 | raise Exception("This should not be called") 66 | 67 | async with Client(fastmcp_server, progress_handler=bad_progress_handler) as client: 68 | await client.call_tool("progress_tool", {}, progress_handler=progress_handler) 69 | 70 | assert PROGRESS_MESSAGES == EXPECTED_PROGRESS_MESSAGES 71 | -------------------------------------------------------------------------------- /tests/client/test_roots.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | 5 | from fastmcp import Client, Context, FastMCP 6 | 7 | 8 | @pytest.fixture 9 | def fastmcp_server(): 10 | mcp = FastMCP() 11 | 12 | @mcp.tool() 13 | async def list_roots(context: Context) -> list[str]: 14 | roots = await context.list_roots() 15 | return [str(r.uri) for r in roots] 16 | 17 | return mcp 18 | 19 | 20 | class TestClientRoots: 21 | @pytest.mark.parametrize("roots", [["x"], ["x", "y"]]) 22 | async def test_invalid_roots(self, fastmcp_server: FastMCP, roots: list[str]): 23 | """ 24 | Roots must be URIs 25 | """ 26 | with pytest.raises(ValueError, match="Input should be a valid URL"): 27 | async with Client(fastmcp_server, roots=roots): 28 | pass 29 | 30 | @pytest.mark.parametrize("roots", [["https://x.com"]]) 31 | async def test_invalid_urls(self, fastmcp_server: FastMCP, roots: list[str]): 32 | """ 33 | At this time, root URIs must start with file:// 34 | """ 35 | with pytest.raises(ValueError, match="URL scheme should be 'file'"): 36 | async with Client(fastmcp_server, roots=roots): 37 | pass 38 | 39 | @pytest.mark.parametrize("roots", [["file://x/y/z", "file://x/y/z"]]) 40 | async def test_valid_roots(self, fastmcp_server: FastMCP, roots: list[str]): 41 | async with Client(fastmcp_server, roots=roots) as client: 42 | result = await client.call_tool("list_roots", {}) 43 | assert json.loads(result[0].text) == [ # type: ignore[attr-defined] 44 | "file://x/y/z", 45 | "file://x/y/z", 46 | ] 47 | -------------------------------------------------------------------------------- /tests/client/test_sampling.py: -------------------------------------------------------------------------------- 1 | from typing import cast 2 | 3 | import pytest 4 | from mcp.types import TextContent 5 | 6 | from fastmcp import Client, Context, FastMCP 7 | from fastmcp.client.sampling import RequestContext, SamplingMessage, SamplingParams 8 | 9 | 10 | @pytest.fixture 11 | def fastmcp_server(): 12 | mcp = FastMCP() 13 | 14 | @mcp.tool() 15 | async def simple_sample(message: str, context: Context) -> str: 16 | result = await context.sample("Hello, world!") 17 | return cast(TextContent, result).text 18 | 19 | @mcp.tool() 20 | async def sample_with_system_prompt(message: str, context: Context) -> str: 21 | result = await context.sample("Hello, world!", system_prompt="You love FastMCP") 22 | return cast(TextContent, result).text 23 | 24 | @mcp.tool() 25 | async def sample_with_messages(message: str, context: Context) -> str: 26 | result = await context.sample( 27 | [ 28 | "Hello!", 29 | SamplingMessage( 30 | content=TextContent( 31 | type="text", text="How can I assist you today?" 32 | ), 33 | role="assistant", 34 | ), 35 | ] 36 | ) 37 | return cast(TextContent, result).text 38 | 39 | return mcp 40 | 41 | 42 | async def test_simple_sampling(fastmcp_server: FastMCP): 43 | def sampling_handler( 44 | messages: list[SamplingMessage], params: SamplingParams, ctx: RequestContext 45 | ) -> str: 46 | return "This is the sample message!" 47 | 48 | async with Client(fastmcp_server, sampling_handler=sampling_handler) as client: 49 | result = await client.call_tool("simple_sample", {"message": "Hello, world!"}) 50 | reply = cast(TextContent, result[0]) 51 | assert reply.text == "This is the sample message!" 52 | 53 | 54 | async def test_sampling_with_system_prompt(fastmcp_server: FastMCP): 55 | def sampling_handler( 56 | messages: list[SamplingMessage], params: SamplingParams, ctx: RequestContext 57 | ) -> str: 58 | assert params.systemPrompt is not None 59 | return params.systemPrompt 60 | 61 | async with Client(fastmcp_server, sampling_handler=sampling_handler) as client: 62 | result = await client.call_tool( 63 | "sample_with_system_prompt", {"message": "Hello, world!"} 64 | ) 65 | reply = cast(TextContent, result[0]) 66 | assert reply.text == "You love FastMCP" 67 | 68 | 69 | async def test_sampling_with_messages(fastmcp_server: FastMCP): 70 | def sampling_handler( 71 | messages: list[SamplingMessage], params: SamplingParams, ctx: RequestContext 72 | ) -> str: 73 | assert len(messages) == 2 74 | assert messages[0].content.type == "text" 75 | assert messages[0].content.text == "Hello!" 76 | assert messages[1].content.type == "text" 77 | assert messages[1].content.text == "How can I assist you today?" 78 | return "I need to think." 79 | 80 | async with Client(fastmcp_server, sampling_handler=sampling_handler) as client: 81 | result = await client.call_tool( 82 | "sample_with_messages", {"message": "Hello, world!"} 83 | ) 84 | reply = cast(TextContent, result[0]) 85 | assert reply.text == "I need to think." 86 | -------------------------------------------------------------------------------- /tests/client/test_stdio.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | import pytest 4 | 5 | from fastmcp import Client 6 | from fastmcp.client.transports import PythonStdioTransport, StdioTransport 7 | 8 | 9 | class TestKeepAlive: 10 | # https://github.com/jlowin/fastmcp/issues/581 11 | 12 | @pytest.fixture 13 | def stdio_script(self, tmp_path): 14 | script = inspect.cleandoc(''' 15 | import os 16 | from fastmcp import FastMCP 17 | 18 | mcp = FastMCP() 19 | 20 | @mcp.tool() 21 | def pid() -> int: 22 | """Gets PID of server""" 23 | return os.getpid() 24 | 25 | if __name__ == "__main__": 26 | mcp.run() 27 | ''') 28 | script_file = tmp_path / "stdio.py" 29 | script_file.write_text(script) 30 | return script_file 31 | 32 | async def test_keep_alive_default_true(self): 33 | client = Client(transport=StdioTransport(command="python", args=[""])) 34 | 35 | assert client.transport.keep_alive is True 36 | 37 | async def test_keep_alive_set_false(self): 38 | client = Client( 39 | transport=StdioTransport(command="python", args=[""], keep_alive=False) 40 | ) 41 | assert client.transport.keep_alive is False 42 | 43 | async def test_keep_alive_maintains_session_across_multiple_calls( 44 | self, stdio_script 45 | ): 46 | client = Client(transport=PythonStdioTransport(script_path=stdio_script)) 47 | assert client.transport.keep_alive is True 48 | 49 | async with client: 50 | result1 = await client.call_tool("pid") 51 | pid1 = int(result1[0].text) # type: ignore[attr-defined] 52 | 53 | async with client: 54 | result2 = await client.call_tool("pid") 55 | pid2 = int(result2[0].text) # type: ignore[attr-defined] 56 | 57 | assert pid1 == pid2 58 | 59 | async def test_keep_alive_false_starts_new_session_across_multiple_calls( 60 | self, stdio_script 61 | ): 62 | client = Client( 63 | transport=PythonStdioTransport(script_path=stdio_script, keep_alive=False) 64 | ) 65 | assert client.transport.keep_alive is False 66 | 67 | async with client: 68 | result1 = await client.call_tool("pid") 69 | pid1 = int(result1[0].text) # type: ignore[attr-defined] 70 | 71 | async with client: 72 | result2 = await client.call_tool("pid") 73 | pid2 = int(result2[0].text) # type: ignore[attr-defined] 74 | 75 | assert pid1 != pid2 76 | 77 | async def test_keep_alive_starts_new_session_if_manually_closed(self, stdio_script): 78 | client = Client(transport=PythonStdioTransport(script_path=stdio_script)) 79 | assert client.transport.keep_alive is True 80 | 81 | async with client: 82 | result1 = await client.call_tool("pid") 83 | pid1 = int(result1[0].text) # type: ignore[attr-defined] 84 | 85 | await client.close() 86 | 87 | async with client: 88 | result2 = await client.call_tool("pid") 89 | pid2 = int(result2[0].text) # type: ignore[attr-defined] 90 | 91 | assert pid1 != pid2 92 | 93 | async def test_keep_alive_maintains_session_if_reentered(self, stdio_script): 94 | client = Client(transport=PythonStdioTransport(script_path=stdio_script)) 95 | assert client.transport.keep_alive is True 96 | 97 | async with client: 98 | result1 = await client.call_tool("pid") 99 | pid1 = int(result1[0].text) # type: ignore[attr-defined] 100 | 101 | async with client: 102 | result2 = await client.call_tool("pid") 103 | pid2 = int(result2[0].text) # type: ignore[attr-defined] 104 | 105 | result3 = await client.call_tool("pid") 106 | pid3 = int(result3[0].text) # type: ignore[attr-defined] 107 | 108 | assert pid1 == pid2 == pid3 109 | 110 | async def test_close_session_and_try_to_use_client_raises_error(self, stdio_script): 111 | client = Client(transport=PythonStdioTransport(script_path=stdio_script)) 112 | assert client.transport.keep_alive is True 113 | 114 | async with client: 115 | await client.close() 116 | with pytest.raises(RuntimeError, match="Client is not connected"): 117 | await client.call_tool("pid") 118 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlowin/fastmcp/97ef80c62478d0f366d62baf3e15875937cd7b9f/tests/conftest.py -------------------------------------------------------------------------------- /tests/contrib/__init__.py: -------------------------------------------------------------------------------- 1 | # This file makes Python treat the directory as a package. 2 | -------------------------------------------------------------------------------- /tests/deprecated/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlowin/fastmcp/97ef80c62478d0f366d62baf3e15875937cd7b9f/tests/deprecated/__init__.py -------------------------------------------------------------------------------- /tests/deprecated/test_mount_separators.py: -------------------------------------------------------------------------------- 1 | """Tests for the deprecated separator parameters in mount() and import_server() methods.""" 2 | 3 | import pytest 4 | 5 | from fastmcp import FastMCP 6 | 7 | 8 | def test_mount_tool_separator_deprecation_warning(): 9 | """Test that using tool_separator in mount() raises a deprecation warning.""" 10 | main_app = FastMCP("MainApp") 11 | sub_app = FastMCP("SubApp") 12 | 13 | with pytest.warns( 14 | DeprecationWarning, 15 | match="The tool_separator parameter is deprecated and will be removed in a future version", 16 | ): 17 | main_app.mount("sub", sub_app, tool_separator="-") 18 | 19 | # Verify the separator is ignored and the default is used 20 | @sub_app.tool() 21 | def test_tool(): 22 | return "test" 23 | 24 | mounted_server = main_app._mounted_servers["sub"] 25 | assert mounted_server.match_tool("sub_test_tool") 26 | assert not mounted_server.match_tool("sub-test_tool") 27 | 28 | 29 | def test_mount_resource_separator_deprecation_warning(): 30 | """Test that using resource_separator in mount() raises a deprecation warning.""" 31 | main_app = FastMCP("MainApp") 32 | sub_app = FastMCP("SubApp") 33 | 34 | with pytest.warns( 35 | DeprecationWarning, 36 | match="The resource_separator parameter is deprecated and ignored", 37 | ): 38 | main_app.mount("sub", sub_app, resource_separator="+") 39 | 40 | 41 | def test_mount_prompt_separator_deprecation_warning(): 42 | """Test that using prompt_separator in mount() raises a deprecation warning.""" 43 | main_app = FastMCP("MainApp") 44 | sub_app = FastMCP("SubApp") 45 | 46 | with pytest.warns( 47 | DeprecationWarning, 48 | match="The prompt_separator parameter is deprecated and will be removed in a future version", 49 | ): 50 | main_app.mount("sub", sub_app, prompt_separator="-") 51 | 52 | # Verify the separator is ignored and the default is used 53 | @sub_app.prompt() 54 | def test_prompt(): 55 | return "test" 56 | 57 | mounted_server = main_app._mounted_servers["sub"] 58 | assert mounted_server.match_prompt("sub_test_prompt") 59 | assert not mounted_server.match_prompt("sub-test_prompt") 60 | 61 | 62 | async def test_import_server_separator_deprecation_warnings(): 63 | """Test that using separators in import_server() raises deprecation warnings.""" 64 | main_app = FastMCP("MainApp") 65 | sub_app = FastMCP("SubApp") 66 | 67 | with pytest.warns( 68 | DeprecationWarning, 69 | match="The tool_separator parameter is deprecated and will be removed in a future version", 70 | ): 71 | await main_app.import_server("sub", sub_app, tool_separator="-") 72 | 73 | main_app = FastMCP("MainApp") 74 | with pytest.warns( 75 | DeprecationWarning, 76 | match="The resource_separator parameter is deprecated and ignored", 77 | ): 78 | await main_app.import_server("sub", sub_app, resource_separator="+") 79 | 80 | main_app = FastMCP("MainApp") 81 | with pytest.warns( 82 | DeprecationWarning, 83 | match="The prompt_separator parameter is deprecated and will be removed in a future version", 84 | ): 85 | await main_app.import_server("sub", sub_app, prompt_separator="-") 86 | -------------------------------------------------------------------------------- /tests/deprecated/test_resource_prefixes.py: -------------------------------------------------------------------------------- 1 | """Tests for legacy resource prefix behavior.""" 2 | 3 | from fastmcp import Client, FastMCP 4 | from fastmcp.server.server import ( 5 | add_resource_prefix, 6 | has_resource_prefix, 7 | remove_resource_prefix, 8 | ) 9 | from fastmcp.utilities.tests import temporary_settings 10 | 11 | 12 | class TestLegacyResourcePrefixes: 13 | """Test the legacy resource prefix behavior.""" 14 | 15 | def test_add_resource_prefix_legacy(self): 16 | """Test that add_resource_prefix uses the legacy format when resource_prefix_format is 'protocol'.""" 17 | with temporary_settings(resource_prefix_format="protocol"): 18 | result = add_resource_prefix("resource://path/to/resource", "prefix") 19 | assert result == "prefix+resource://path/to/resource" 20 | 21 | # Empty prefix should return the original URI 22 | result = add_resource_prefix("resource://path/to/resource", "") 23 | assert result == "resource://path/to/resource" 24 | 25 | def test_remove_resource_prefix_legacy(self): 26 | """Test that remove_resource_prefix uses the legacy format when resource_prefix_format is 'protocol'.""" 27 | with temporary_settings(resource_prefix_format="protocol"): 28 | result = remove_resource_prefix( 29 | "prefix+resource://path/to/resource", "prefix" 30 | ) 31 | assert result == "resource://path/to/resource" 32 | 33 | # URI without the prefix should be returned as is 34 | result = remove_resource_prefix("resource://path/to/resource", "prefix") 35 | assert result == "resource://path/to/resource" 36 | 37 | # Empty prefix should return the original URI 38 | result = remove_resource_prefix("resource://path/to/resource", "") 39 | assert result == "resource://path/to/resource" 40 | 41 | def test_has_resource_prefix_legacy(self): 42 | """Test that has_resource_prefix uses the legacy format when resource_prefix_format is 'protocol'.""" 43 | with temporary_settings(resource_prefix_format="protocol"): 44 | result = has_resource_prefix("prefix+resource://path/to/resource", "prefix") 45 | assert result is True 46 | 47 | result = has_resource_prefix("resource://path/to/resource", "prefix") 48 | assert result is False 49 | 50 | # Empty prefix should always return False 51 | result = has_resource_prefix("resource://path/to/resource", "") 52 | assert result is False 53 | 54 | 55 | async def test_mount_with_legacy_prefixes(): 56 | """Test mounting a server with legacy resource prefixes.""" 57 | with temporary_settings(resource_prefix_format="protocol"): 58 | main_server = FastMCP("MainServer") 59 | sub_server = FastMCP("SubServer") 60 | 61 | @sub_server.resource("resource://test") 62 | def get_test(): 63 | return "test content" 64 | 65 | # Mount the server with a prefix 66 | main_server.mount("sub", sub_server) 67 | 68 | # Check that the resource is prefixed using the legacy format 69 | resources = await main_server.get_resources() 70 | 71 | # In legacy format, the key would be "sub+resource://test" 72 | assert "sub+resource://test" in resources 73 | 74 | # Test accessing the resource through client 75 | async with Client(main_server) as client: 76 | result = await client.read_resource("sub+resource://test") 77 | # Different content types might be returned, but we just want to verify we got something 78 | assert len(result) > 0 79 | 80 | 81 | async def test_import_server_with_legacy_prefixes(): 82 | """Test importing a server with legacy resource prefixes.""" 83 | with temporary_settings(resource_prefix_format="protocol"): 84 | main_server = FastMCP("MainServer") 85 | sub_server = FastMCP("SubServer") 86 | 87 | @sub_server.resource("resource://test") 88 | def get_test(): 89 | return "test content" 90 | 91 | # Import the server with a prefix 92 | await main_server.import_server("sub", sub_server) 93 | 94 | # Check that the resource is prefixed using the legacy format 95 | resources = main_server._resource_manager.get_resources() 96 | 97 | # In legacy format, the key would be "sub+resource://test" 98 | assert "sub+resource://test" in resources 99 | -------------------------------------------------------------------------------- /tests/deprecated/test_route_type_ignore.py: -------------------------------------------------------------------------------- 1 | """Tests for the deprecated RouteType.IGNORE.""" 2 | 3 | import warnings 4 | 5 | import httpx 6 | import pytest 7 | 8 | from fastmcp.server.openapi import ( 9 | FastMCPOpenAPI, 10 | MCPType, 11 | RouteMap, 12 | RouteType, 13 | ) 14 | 15 | 16 | def test_route_type_ignore_deprecation_warning(): 17 | """Test that using RouteType.IGNORE emits a deprecation warning.""" 18 | # Let's manually capture the warnings 19 | 20 | # Record all warnings 21 | with warnings.catch_warnings(record=True) as recorded: 22 | # Make sure warnings are always triggered 23 | warnings.simplefilter("always") 24 | 25 | # Create a RouteMap with RouteType.IGNORE 26 | route_map = RouteMap( 27 | methods=["GET"], pattern=r"^/analytics$", route_type=RouteType.IGNORE 28 | ) 29 | 30 | # Check for the expected warnings in the recorded warnings 31 | route_type_warning = False 32 | ignore_warning = False 33 | 34 | for w in recorded: 35 | if issubclass(w.category, DeprecationWarning): 36 | message = str(w.message) 37 | if "route_type' parameter is deprecated" in message: 38 | route_type_warning = True 39 | if "RouteType.IGNORE is deprecated" in message: 40 | ignore_warning = True 41 | 42 | # Make sure both warnings were triggered 43 | assert route_type_warning, "Missing 'route_type' deprecation warning" 44 | assert ignore_warning, "Missing 'RouteType.IGNORE' deprecation warning" 45 | 46 | # Verify that RouteType.IGNORE was converted to MCPType.EXCLUDE 47 | assert route_map.mcp_type == MCPType.EXCLUDE 48 | 49 | 50 | class TestRouteTypeIgnoreDeprecation: 51 | """Test class for the deprecated RouteType.IGNORE.""" 52 | 53 | @pytest.fixture 54 | def basic_openapi_spec(self) -> dict: 55 | """Create a simple OpenAPI spec for testing.""" 56 | return { 57 | "openapi": "3.0.0", 58 | "info": {"title": "Test API", "version": "1.0.0"}, 59 | "paths": { 60 | "/items": { 61 | "get": { 62 | "operationId": "get_items", 63 | "summary": "Get all items", 64 | "responses": {"200": {"description": "Success"}}, 65 | } 66 | }, 67 | "/analytics": { 68 | "get": { 69 | "operationId": "get_analytics", 70 | "summary": "Get analytics data", 71 | "responses": {"200": {"description": "Success"}}, 72 | } 73 | }, 74 | }, 75 | } 76 | 77 | @pytest.fixture 78 | async def mock_client(self) -> httpx.AsyncClient: 79 | """Create a mock client for testing.""" 80 | 81 | async def _responder(request): 82 | return httpx.Response(200, json={"success": True}) 83 | 84 | return httpx.AsyncClient(transport=httpx.MockTransport(_responder)) 85 | 86 | async def test_route_type_ignore_conversion(self, basic_openapi_spec, mock_client): 87 | """Test that routes with RouteType.IGNORE are properly excluded.""" 88 | # Capture the deprecation warning without checking the exact message 89 | with pytest.warns(DeprecationWarning): 90 | server = FastMCPOpenAPI( 91 | openapi_spec=basic_openapi_spec, 92 | client=mock_client, 93 | route_maps=[ 94 | # Use the deprecated RouteType.IGNORE 95 | RouteMap( 96 | methods=["GET"], 97 | pattern=r"^/analytics$", 98 | route_type=RouteType.IGNORE, 99 | ), 100 | # Make everything else a resource 101 | RouteMap( 102 | methods=["GET"], pattern=r".*", route_type=RouteType.RESOURCE 103 | ), 104 | ], 105 | ) 106 | 107 | # Check that the analytics route was excluded (converted from IGNORE to EXCLUDE) 108 | resources = await server.get_resources() 109 | resource_uris = [str(r.uri) for r in resources.values()] 110 | 111 | # Analytics should be excluded 112 | assert "resource://get_items" in resource_uris 113 | assert "resource://get_analytics" not in resource_uris 114 | -------------------------------------------------------------------------------- /tests/prompts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlowin/fastmcp/97ef80c62478d0f366d62baf3e15875937cd7b9f/tests/prompts/__init__.py -------------------------------------------------------------------------------- /tests/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlowin/fastmcp/97ef80c62478d0f366d62baf3e15875937cd7b9f/tests/resources/__init__.py -------------------------------------------------------------------------------- /tests/resources/test_file_resources.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | from tempfile import NamedTemporaryFile 4 | 5 | import pytest 6 | from pydantic import FileUrl 7 | 8 | from fastmcp.exceptions import ResourceError 9 | from fastmcp.resources import FileResource 10 | 11 | 12 | @pytest.fixture 13 | def temp_file(): 14 | """Create a temporary file for testing. 15 | 16 | File is automatically cleaned up after the test if it still exists. 17 | """ 18 | content = "test content" 19 | with NamedTemporaryFile(mode="w", delete=False) as f: 20 | f.write(content) 21 | path = Path(f.name).resolve() 22 | yield path 23 | try: 24 | path.unlink() 25 | except FileNotFoundError: 26 | pass # File was already deleted by the test 27 | 28 | 29 | class TestFileResource: 30 | """Test FileResource functionality.""" 31 | 32 | def test_file_resource_creation(self, temp_file: Path): 33 | """Test creating a FileResource.""" 34 | resource = FileResource( 35 | uri=FileUrl(temp_file.as_uri()), 36 | name="test", 37 | description="test file", 38 | path=temp_file, 39 | ) 40 | assert str(resource.uri) == temp_file.as_uri() 41 | assert resource.name == "test" 42 | assert resource.description == "test file" 43 | assert resource.mime_type == "text/plain" # default 44 | assert resource.path == temp_file 45 | assert resource.is_binary is False # default 46 | 47 | def test_file_resource_str_path_conversion(self, temp_file: Path): 48 | """Test FileResource handles string paths.""" 49 | resource = FileResource( 50 | uri=FileUrl(f"file://{temp_file}"), 51 | name="test", 52 | path=Path(str(temp_file)), 53 | ) 54 | assert isinstance(resource.path, Path) 55 | assert resource.path.is_absolute() 56 | 57 | async def test_read_text_file(self, temp_file: Path): 58 | """Test reading a text file.""" 59 | resource = FileResource( 60 | uri=FileUrl(f"file://{temp_file}"), 61 | name="test", 62 | path=temp_file, 63 | ) 64 | content = await resource.read() 65 | assert content == "test content" 66 | assert resource.mime_type == "text/plain" 67 | 68 | async def test_read_binary_file(self, temp_file: Path): 69 | """Test reading a file as binary.""" 70 | resource = FileResource( 71 | uri=FileUrl(f"file://{temp_file}"), 72 | name="test", 73 | path=temp_file, 74 | is_binary=True, 75 | ) 76 | content = await resource.read() 77 | assert content == b"test content" 78 | 79 | def test_relative_path_error(self): 80 | """Test error on relative path.""" 81 | with pytest.raises(ValueError, match="Path must be absolute"): 82 | FileResource( 83 | uri=FileUrl("file:///test.txt"), 84 | name="test", 85 | path=Path("test.txt"), 86 | ) 87 | 88 | async def test_missing_file_error(self, temp_file: Path): 89 | """Test error when file doesn't exist.""" 90 | # Create path to non-existent file 91 | missing = temp_file.parent / "missing.txt" 92 | resource = FileResource( 93 | uri=FileUrl("file:///missing.txt"), 94 | name="test", 95 | path=missing, 96 | ) 97 | with pytest.raises(ResourceError, match="Error reading file"): 98 | await resource.read() 99 | 100 | @pytest.mark.skipif( 101 | os.name == "nt" or (hasattr(os, "getuid") and os.getuid() == 0), 102 | reason="File permissions behave differently on Windows or when running as root", 103 | ) 104 | async def test_permission_error(self, temp_file: Path): 105 | """Test reading a file without permissions.""" 106 | temp_file.chmod(0o000) # Remove all permissions 107 | try: 108 | resource = FileResource( 109 | uri=FileUrl(temp_file.as_uri()), 110 | name="test", 111 | path=temp_file, 112 | ) 113 | with pytest.raises(ResourceError, match="Error reading file"): 114 | await resource.read() 115 | finally: 116 | temp_file.chmod(0o644) # Restore permissions 117 | -------------------------------------------------------------------------------- /tests/resources/test_function_resources.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pydantic import AnyUrl, BaseModel 3 | 4 | from fastmcp.resources import FunctionResource 5 | 6 | 7 | class TestFunctionResource: 8 | """Test FunctionResource functionality.""" 9 | 10 | def test_function_resource_creation(self): 11 | """Test creating a FunctionResource.""" 12 | 13 | def my_func() -> str: 14 | return "test content" 15 | 16 | resource = FunctionResource( 17 | uri=AnyUrl("fn://test"), 18 | name="test", 19 | description="test function", 20 | fn=my_func, 21 | ) 22 | assert str(resource.uri) == "fn://test" 23 | assert resource.name == "test" 24 | assert resource.description == "test function" 25 | assert resource.mime_type == "text/plain" # default 26 | assert resource.fn == my_func 27 | 28 | async def test_read_text(self): 29 | """Test reading text from a FunctionResource.""" 30 | 31 | def get_data() -> str: 32 | return "Hello, world!" 33 | 34 | resource = FunctionResource( 35 | uri=AnyUrl("function://test"), 36 | name="test", 37 | fn=get_data, 38 | ) 39 | content = await resource.read() 40 | assert content == "Hello, world!" 41 | assert resource.mime_type == "text/plain" 42 | 43 | async def test_read_binary(self): 44 | """Test reading binary data from a FunctionResource.""" 45 | 46 | def get_data() -> bytes: 47 | return b"Hello, world!" 48 | 49 | resource = FunctionResource( 50 | uri=AnyUrl("function://test"), 51 | name="test", 52 | fn=get_data, 53 | ) 54 | content = await resource.read() 55 | assert content == b"Hello, world!" 56 | 57 | async def test_json_conversion(self): 58 | """Test automatic JSON conversion of non-string results.""" 59 | 60 | def get_data() -> dict: 61 | return {"key": "value"} 62 | 63 | resource = FunctionResource( 64 | uri=AnyUrl("function://test"), 65 | name="test", 66 | fn=get_data, 67 | ) 68 | content = await resource.read() 69 | assert isinstance(content, str) 70 | assert '"key": "value"' in content 71 | 72 | async def test_error_handling(self): 73 | """Test error handling in FunctionResource.""" 74 | 75 | def failing_func() -> str: 76 | raise ValueError("Test error") 77 | 78 | resource = FunctionResource( 79 | uri=AnyUrl("function://test"), 80 | name="test", 81 | fn=failing_func, 82 | ) 83 | with pytest.raises(ValueError, match="Test error"): 84 | await resource.read() 85 | 86 | async def test_basemodel_conversion(self): 87 | """Test handling of BaseModel types.""" 88 | 89 | class MyModel(BaseModel): 90 | name: str 91 | 92 | resource = FunctionResource( 93 | uri=AnyUrl("function://test"), 94 | name="test", 95 | fn=lambda: MyModel(name="test"), 96 | ) 97 | content = await resource.read() 98 | assert content == '{\n "name": "test"\n}' 99 | 100 | async def test_custom_type_conversion(self): 101 | """Test handling of custom types.""" 102 | 103 | class CustomData: 104 | def __str__(self) -> str: 105 | return "custom data" 106 | 107 | def get_data() -> CustomData: 108 | return CustomData() 109 | 110 | resource = FunctionResource( 111 | uri=AnyUrl("function://test"), 112 | name="test", 113 | fn=get_data, 114 | ) 115 | content = await resource.read() 116 | assert isinstance(content, str) 117 | 118 | async def test_async_read_text(self): 119 | """Test reading text from async FunctionResource.""" 120 | 121 | async def get_data() -> str: 122 | return "Hello, world!" 123 | 124 | resource = FunctionResource( 125 | uri=AnyUrl("function://test"), 126 | name="test", 127 | fn=get_data, 128 | ) 129 | content = await resource.read() 130 | assert content == "Hello, world!" 131 | assert resource.mime_type == "text/plain" 132 | -------------------------------------------------------------------------------- /tests/resources/test_resources.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pydantic import AnyUrl 3 | 4 | from fastmcp.resources import FunctionResource, Resource 5 | 6 | 7 | class TestResourceValidation: 8 | """Test base Resource validation.""" 9 | 10 | def test_resource_uri_validation(self): 11 | """Test URI validation.""" 12 | 13 | def dummy_func() -> str: 14 | return "data" 15 | 16 | # Valid URI 17 | resource = FunctionResource( 18 | uri=AnyUrl("http://example.com/data"), 19 | name="test", 20 | fn=dummy_func, 21 | ) 22 | assert str(resource.uri) == "http://example.com/data" 23 | 24 | # Missing protocol 25 | with pytest.raises(ValueError, match="Input should be a valid URL"): 26 | FunctionResource( 27 | uri=AnyUrl("invalid"), 28 | name="test", 29 | fn=dummy_func, 30 | ) 31 | 32 | # Missing host 33 | with pytest.raises(ValueError, match="Input should be a valid URL"): 34 | FunctionResource( 35 | uri=AnyUrl("http://"), 36 | name="test", 37 | fn=dummy_func, 38 | ) 39 | 40 | def test_resource_name_from_uri(self): 41 | """Test name is extracted from URI if not provided.""" 42 | 43 | def dummy_func() -> str: 44 | return "data" 45 | 46 | resource = FunctionResource( 47 | uri=AnyUrl("resource://my-resource"), 48 | fn=dummy_func, 49 | ) 50 | assert resource.name == "resource://my-resource" 51 | 52 | def test_resource_name_validation(self): 53 | """Test name validation.""" 54 | 55 | def dummy_func() -> str: 56 | return "data" 57 | 58 | # Must provide either name or URI 59 | with pytest.raises(ValueError, match="Either name or uri must be provided"): 60 | FunctionResource( 61 | fn=dummy_func, 62 | ) 63 | 64 | # Explicit name takes precedence over URI 65 | resource = FunctionResource( 66 | uri=AnyUrl("resource://uri-name"), 67 | name="explicit-name", 68 | fn=dummy_func, 69 | ) 70 | assert resource.name == "explicit-name" 71 | 72 | def test_resource_mime_type(self): 73 | """Test mime type handling.""" 74 | 75 | def dummy_func() -> str: 76 | return "data" 77 | 78 | # Default mime type 79 | resource = FunctionResource( 80 | uri=AnyUrl("resource://test"), 81 | fn=dummy_func, 82 | ) 83 | assert resource.mime_type == "text/plain" 84 | 85 | # Custom mime type 86 | resource = FunctionResource( 87 | uri=AnyUrl("resource://test"), 88 | fn=dummy_func, 89 | mime_type="application/json", 90 | ) 91 | assert resource.mime_type == "application/json" 92 | 93 | async def test_resource_read_abstract(self): 94 | """Test that Resource.read() is abstract.""" 95 | 96 | class ConcreteResource(Resource): 97 | pass 98 | 99 | with pytest.raises(TypeError, match="abstract method"): 100 | ConcreteResource(uri=AnyUrl("test://test"), name="test") # type: ignore 101 | -------------------------------------------------------------------------------- /tests/server/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlowin/fastmcp/97ef80c62478d0f366d62baf3e15875937cd7b9f/tests/server/__init__.py -------------------------------------------------------------------------------- /tests/server/http/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlowin/fastmcp/97ef80c62478d0f366d62baf3e15875937cd7b9f/tests/server/http/__init__.py -------------------------------------------------------------------------------- /tests/server/http/test_custom_routes.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from starlette.requests import Request 3 | from starlette.responses import JSONResponse 4 | from starlette.routing import Route 5 | 6 | from fastmcp import FastMCP 7 | from fastmcp.server.http import create_sse_app, create_streamable_http_app 8 | 9 | 10 | class TestCustomRoutes: 11 | @pytest.fixture 12 | def server_with_custom_route(self): 13 | """Create a FastMCP server with a custom route.""" 14 | server = FastMCP() 15 | 16 | @server.custom_route("/custom-route", methods=["GET"]) 17 | async def custom_route(request: Request): 18 | return JSONResponse({"message": "custom route"}) 19 | 20 | return server 21 | 22 | def test_custom_routes_via_server_http_app(self, server_with_custom_route): 23 | """Test that custom routes are included when using server.http_app().""" 24 | # Get the app via server.http_app() 25 | app = server_with_custom_route.http_app() 26 | 27 | # Verify that the custom route is included 28 | custom_route_found = False 29 | for route in app.routes: 30 | if isinstance(route, Route) and route.path == "/custom-route": 31 | custom_route_found = True 32 | break 33 | 34 | assert custom_route_found, "Custom route was not found in app routes" 35 | 36 | def test_custom_routes_via_streamable_http_app_direct( 37 | self, server_with_custom_route 38 | ): 39 | """Test that custom routes are included when using create_streamable_http_app directly.""" 40 | # Create the app by calling the constructor function directly 41 | app = create_streamable_http_app( 42 | server=server_with_custom_route, streamable_http_path="/api" 43 | ) 44 | 45 | # Verify that the custom route is included 46 | custom_route_found = False 47 | for route in app.routes: 48 | if isinstance(route, Route) and route.path == "/custom-route": 49 | custom_route_found = True 50 | break 51 | 52 | assert custom_route_found, "Custom route was not found in app routes" 53 | 54 | def test_custom_routes_via_sse_app_direct(self, server_with_custom_route): 55 | """Test that custom routes are included when using create_sse_app directly.""" 56 | # Create the app by calling the constructor function directly 57 | app = create_sse_app( 58 | server=server_with_custom_route, message_path="/message", sse_path="/sse" 59 | ) 60 | 61 | # Verify that the custom route is included 62 | custom_route_found = False 63 | for route in app.routes: 64 | if isinstance(route, Route) and route.path == "/custom-route": 65 | custom_route_found = True 66 | break 67 | 68 | assert custom_route_found, "Custom route was not found in app routes" 69 | 70 | def test_multiple_custom_routes( 71 | self, 72 | ): 73 | """Test that multiple custom routes are included in both methods.""" 74 | server = FastMCP() 75 | 76 | custom_paths = ["/route1", "/route2", "/route3"] 77 | 78 | # Add multiple custom routes 79 | for path in custom_paths: 80 | 81 | @server.custom_route(path, methods=["GET"]) 82 | async def custom_route(request: Request): 83 | return JSONResponse({"message": f"route {path}"}) 84 | 85 | # Test with server.http_app() 86 | app1 = server.http_app() 87 | 88 | # Test with direct constructor call 89 | app2 = create_streamable_http_app(server=server, streamable_http_path="/api") 90 | 91 | # Check all routes are in both apps 92 | for path in custom_paths: 93 | # Check in app1 94 | route_in_app1 = any( 95 | isinstance(route, Route) and route.path == path for route in app1.routes 96 | ) 97 | assert route_in_app1, f"Route {path} not found in server.http_app()" 98 | 99 | # Check in app2 100 | route_in_app2 = any( 101 | isinstance(route, Route) and route.path == path for route in app2.routes 102 | ) 103 | assert route_in_app2, ( 104 | f"Route {path} not found in create_streamable_http_app()" 105 | ) 106 | -------------------------------------------------------------------------------- /tests/server/openapi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlowin/fastmcp/97ef80c62478d0f366d62baf3e15875937cd7b9f/tests/server/openapi/__init__.py -------------------------------------------------------------------------------- /tests/server/test_app_state.py: -------------------------------------------------------------------------------- 1 | from fastmcp.server import FastMCP 2 | from fastmcp.server.http import create_sse_app, create_streamable_http_app 3 | 4 | 5 | def test_http_app_sets_mcp_server_state(): 6 | server = FastMCP(name="StateTest") 7 | app = server.http_app() 8 | assert app.state.fastmcp_server is server 9 | 10 | 11 | def test_http_app_sse_sets_mcp_server_state(): 12 | server = FastMCP(name="StateTest") 13 | app = server.http_app(transport="sse") 14 | assert app.state.fastmcp_server is server 15 | 16 | 17 | def test_create_streamable_http_app_sets_state(): 18 | server = FastMCP(name="StateTest") 19 | app = create_streamable_http_app(server, "/mcp") 20 | assert app.state.fastmcp_server is server 21 | 22 | 23 | def test_create_sse_app_sets_state(): 24 | server = FastMCP(name="StateTest") 25 | app = create_sse_app(server, message_path="/message", sse_path="/sse") 26 | assert app.state.fastmcp_server is server 27 | -------------------------------------------------------------------------------- /tests/server/test_context.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | from unittest.mock import MagicMock, patch 3 | 4 | import pytest 5 | from mcp.types import ModelPreferences 6 | from starlette.requests import Request 7 | 8 | from fastmcp.server.context import Context 9 | from fastmcp.server.server import FastMCP 10 | 11 | 12 | class TestContextDeprecations: 13 | def test_get_http_request_deprecation_warning(self): 14 | """Test that using Context.get_http_request() raises a deprecation warning.""" 15 | # Create a mock FastMCP instance 16 | mock_fastmcp = MagicMock() 17 | context = Context(fastmcp=mock_fastmcp) 18 | 19 | # Patch the dependency function to return a mock request 20 | mock_request = MagicMock(spec=Request) 21 | with patch( 22 | "fastmcp.server.dependencies.get_http_request", return_value=mock_request 23 | ): 24 | # Check that the deprecation warning is raised 25 | with pytest.warns( 26 | DeprecationWarning, match="Context.get_http_request\\(\\) is deprecated" 27 | ): 28 | request = context.get_http_request() 29 | 30 | # Verify the function still works and returns the request 31 | assert request is mock_request 32 | 33 | def test_get_http_request_deprecation_message(self): 34 | """Test that the deprecation warning has the correct message with guidance.""" 35 | # Create a mock FastMCP instance 36 | mock_fastmcp = MagicMock() 37 | context = Context(fastmcp=mock_fastmcp) 38 | 39 | # Patch the dependency function to return a mock request 40 | mock_request = MagicMock(spec=Request) 41 | with patch( 42 | "fastmcp.server.dependencies.get_http_request", return_value=mock_request 43 | ): 44 | # Capture and check the specific warning message 45 | with warnings.catch_warnings(record=True) as w: 46 | warnings.simplefilter("always") 47 | context.get_http_request() 48 | 49 | assert len(w) == 1 50 | warning = w[0] 51 | assert issubclass(warning.category, DeprecationWarning) 52 | assert "Context.get_http_request() is deprecated" in str( 53 | warning.message 54 | ) 55 | assert ( 56 | "Use get_http_request() from fastmcp.server.dependencies instead" 57 | in str(warning.message) 58 | ) 59 | assert "https://gofastmcp.com/patterns/http-requests" in str( 60 | warning.message 61 | ) 62 | 63 | 64 | @pytest.fixture 65 | def context(): 66 | return Context(fastmcp=FastMCP()) 67 | 68 | 69 | class TestParseModelPreferences: 70 | def test_parse_model_preferences_string(self, context): 71 | mp = context._parse_model_preferences("claude-3-sonnet") 72 | assert isinstance(mp, ModelPreferences) 73 | assert mp.hints is not None 74 | assert mp.hints[0].name == "claude-3-sonnet" 75 | 76 | def test_parse_model_preferences_list(self, context): 77 | mp = context._parse_model_preferences(["claude-3-sonnet", "claude"]) 78 | assert isinstance(mp, ModelPreferences) 79 | assert mp.hints is not None 80 | assert [h.name for h in mp.hints] == ["claude-3-sonnet", "claude"] 81 | 82 | def test_parse_model_preferences_object(self, context): 83 | obj = ModelPreferences(hints=[]) 84 | assert context._parse_model_preferences(obj) is obj 85 | 86 | def test_parse_model_preferences_invalid_type(self, context): 87 | with pytest.raises(ValueError): 88 | context._parse_model_preferences(123) 89 | -------------------------------------------------------------------------------- /tests/server/test_file_server.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | import pytest 5 | 6 | from fastmcp import FastMCP 7 | 8 | 9 | @pytest.fixture() 10 | def test_dir(tmp_path_factory) -> Path: 11 | """Create a temporary directory with test files.""" 12 | tmp = tmp_path_factory.mktemp("test_files") 13 | 14 | # Create test files 15 | (tmp / "example.py").write_text("print('hello world')") 16 | (tmp / "readme.md").write_text("# Test Directory\nThis is a test.") 17 | (tmp / "config.json").write_text('{"test": true}') 18 | 19 | return tmp 20 | 21 | 22 | @pytest.fixture 23 | def mcp() -> FastMCP: 24 | mcp = FastMCP() 25 | 26 | return mcp 27 | 28 | 29 | @pytest.fixture(autouse=True) 30 | def resources(mcp: FastMCP, test_dir: Path) -> FastMCP: 31 | @mcp.resource("dir://test_dir") 32 | def list_test_dir() -> list[str]: 33 | """List the files in the test directory""" 34 | return [str(f) for f in test_dir.iterdir()] 35 | 36 | @mcp.resource("file://test_dir/example.py") 37 | def read_example_py() -> str: 38 | """Read the example.py file""" 39 | try: 40 | return (test_dir / "example.py").read_text() 41 | except FileNotFoundError: 42 | return "File not found" 43 | 44 | @mcp.resource("file://test_dir/readme.md") 45 | def read_readme_md() -> str: 46 | """Read the readme.md file""" 47 | try: 48 | return (test_dir / "readme.md").read_text() 49 | except FileNotFoundError: 50 | return "File not found" 51 | 52 | @mcp.resource("file://test_dir/config.json") 53 | def read_config_json() -> str: 54 | """Read the config.json file""" 55 | try: 56 | return (test_dir / "config.json").read_text() 57 | except FileNotFoundError: 58 | return "File not found" 59 | 60 | return mcp 61 | 62 | 63 | @pytest.fixture(autouse=True) 64 | def tools(mcp: FastMCP, test_dir: Path) -> FastMCP: 65 | @mcp.tool() 66 | def delete_file(path: str) -> bool: 67 | # ensure path is in test_dir 68 | if Path(path).resolve().parent != test_dir: 69 | raise ValueError(f"Path must be in test_dir: {path}") 70 | Path(path).unlink() 71 | return True 72 | 73 | return mcp 74 | 75 | 76 | async def test_list_resources(mcp: FastMCP): 77 | resources = await mcp._mcp_list_resources() 78 | assert len(resources) == 4 79 | 80 | assert [str(r.uri) for r in resources] == [ 81 | "dir://test_dir", 82 | "file://test_dir/example.py", 83 | "file://test_dir/readme.md", 84 | "file://test_dir/config.json", 85 | ] 86 | 87 | 88 | async def test_read_resource_dir(mcp: FastMCP): 89 | res_iter = await mcp._mcp_read_resource("dir://test_dir") 90 | res_list = list(res_iter) 91 | assert len(res_list) == 1 92 | res = res_list[0] 93 | assert res.mime_type == "text/plain" 94 | 95 | files = json.loads(res.content) 96 | 97 | assert sorted([Path(f).name for f in files]) == [ 98 | "config.json", 99 | "example.py", 100 | "readme.md", 101 | ] 102 | 103 | 104 | async def test_read_resource_file(mcp: FastMCP): 105 | res_iter = await mcp._mcp_read_resource("file://test_dir/example.py") 106 | res_list = list(res_iter) 107 | assert len(res_list) == 1 108 | res = res_list[0] 109 | assert res.content == "print('hello world')" 110 | 111 | 112 | async def test_delete_file(mcp: FastMCP, test_dir: Path): 113 | await mcp._mcp_call_tool( 114 | "delete_file", arguments=dict(path=str(test_dir / "example.py")) 115 | ) 116 | assert not (test_dir / "example.py").exists() 117 | 118 | 119 | async def test_delete_file_and_check_resources(mcp: FastMCP, test_dir: Path): 120 | await mcp._mcp_call_tool( 121 | "delete_file", arguments=dict(path=str(test_dir / "example.py")) 122 | ) 123 | res_iter = await mcp._mcp_read_resource("file://test_dir/example.py") 124 | res_list = list(res_iter) 125 | assert len(res_list) == 1 126 | res = res_list[0] 127 | assert res.content == "File not found" 128 | -------------------------------------------------------------------------------- /tests/server/test_resource_prefix_formats.py: -------------------------------------------------------------------------------- 1 | """Tests for different resource prefix formats in server mounting and importing.""" 2 | 3 | from fastmcp import FastMCP 4 | 5 | 6 | async def test_resource_prefix_format_in_constructor(): 7 | """Test that the resource_prefix_format parameter is respected in the constructor.""" 8 | server_path = FastMCP("PathFormat", resource_prefix_format="path") 9 | server_protocol = FastMCP("ProtocolFormat", resource_prefix_format="protocol") 10 | 11 | # Check that the format is stored correctly 12 | assert server_path.resource_prefix_format == "path" 13 | assert server_protocol.resource_prefix_format == "protocol" 14 | 15 | # Register resources 16 | @server_path.resource("resource://test") 17 | def get_test_path(): 18 | return "test content" 19 | 20 | @server_protocol.resource("resource://test") 21 | def get_test_protocol(): 22 | return "test content" 23 | 24 | # Create mount servers 25 | main_server_path = FastMCP("MainPath", resource_prefix_format="path") 26 | main_server_protocol = FastMCP("MainProtocol", resource_prefix_format="protocol") 27 | 28 | # Mount the servers 29 | main_server_path.mount("sub", server_path) 30 | main_server_protocol.mount("sub", server_protocol) 31 | 32 | # Check that the resources are prefixed correctly 33 | path_resources = await main_server_path.get_resources() 34 | protocol_resources = await main_server_protocol.get_resources() 35 | 36 | # Path format should be resource://sub/test 37 | assert "resource://sub/test" in path_resources 38 | # Protocol format should be sub+resource://test 39 | assert "sub+resource://test" in protocol_resources 40 | 41 | 42 | async def test_resource_prefix_format_in_import_server(): 43 | """Test that the resource_prefix_format parameter is respected in import_server.""" 44 | server = FastMCP("TestServer") 45 | 46 | @server.resource("resource://test") 47 | def get_test(): 48 | return "test content" 49 | 50 | # Import with path format 51 | main_server_path = FastMCP("MainPath", resource_prefix_format="path") 52 | await main_server_path.import_server("sub", server) 53 | 54 | # Import with protocol format 55 | main_server_protocol = FastMCP("MainProtocol", resource_prefix_format="protocol") 56 | await main_server_protocol.import_server("sub", server) 57 | 58 | # Check that the resources are prefixed correctly 59 | path_resources = main_server_path._resource_manager.get_resources() 60 | protocol_resources = main_server_protocol._resource_manager.get_resources() 61 | 62 | # Path format should be resource://sub/test 63 | assert "resource://sub/test" in path_resources 64 | # Protocol format should be sub+resource://test 65 | assert "sub+resource://test" in protocol_resources 66 | -------------------------------------------------------------------------------- /tests/server/test_run_server.py: -------------------------------------------------------------------------------- 1 | # from pathlib import Path 2 | # from typing import TYPE_CHECKING, Any 3 | 4 | # import pytest 5 | 6 | # import fastmcp 7 | # from fastmcp import FastMCP 8 | 9 | # if TYPE_CHECKING: 10 | # pass 11 | 12 | # USERS = [ 13 | # {"id": "1", "name": "Alice", "active": True}, 14 | # {"id": "2", "name": "Bob", "active": True}, 15 | # {"id": "3", "name": "Charlie", "active": False}, 16 | # ] 17 | 18 | 19 | # @pytest.fixture 20 | # def fastmcp_server(): 21 | # server = FastMCP("TestServer") 22 | 23 | # # --- Tools --- 24 | 25 | # @server.tool() 26 | # def greet(name: str) -> str: 27 | # """Greet someone by name.""" 28 | # return f"Hello, {name}!" 29 | 30 | # @server.tool() 31 | # def add(a: int, b: int) -> int: 32 | # """Add two numbers together.""" 33 | # return a + b 34 | 35 | # @server.tool() 36 | # def error_tool(): 37 | # """This tool always raises an error.""" 38 | # raise ValueError("This is a test error") 39 | 40 | # # --- Resources --- 41 | 42 | # @server.resource(uri="resource://wave") 43 | # def wave() -> str: 44 | # return "👋" 45 | 46 | # @server.resource(uri="data://users") 47 | # async def get_users() -> list[dict[str, Any]]: 48 | # return USERS 49 | 50 | # @server.resource(uri="data://user/{user_id}") 51 | # async def get_user(user_id: str) -> dict[str, Any] | None: 52 | # return next((user for user in USERS if user["id"] == user_id), None) 53 | 54 | # # --- Prompts --- 55 | 56 | # @server.prompt() 57 | # def welcome(name: str) -> str: 58 | # return f"Welcome to FastMCP, {name}!" 59 | 60 | # return server 61 | 62 | 63 | # @pytest.fixture 64 | # async def stdio_client(): 65 | # # Find the stdio.py script path 66 | # base_dir = Path(__file__).parent 67 | # stdio_script = base_dir / "test_servers" / "stdio.py" 68 | 69 | # if not stdio_script.exists(): 70 | # raise FileNotFoundError(f"Could not find stdio.py script at {stdio_script}") 71 | 72 | # client = fastmcp.Client( 73 | # transport=fastmcp.client.transports.StdioTransport( 74 | # command="python", 75 | # args=[str(stdio_script)], 76 | # ) 77 | # ) 78 | 79 | # async with client: 80 | # print("READY") 81 | # yield client 82 | # print("DONE") 83 | 84 | 85 | # class TestRunServerStdio: 86 | # async def test_run_server_stdio( 87 | # self, fastmcp_server: FastMCP, stdio_client: fastmcp.Client 88 | # ): 89 | # print("TEST") 90 | # tools = await stdio_client.list_tools() 91 | # print("TEST 2") 92 | # assert tools == 1 93 | 94 | 95 | # class TestRunServerSSE: 96 | # 97 | # async def test_run_server_sse(self, fastmcp_server: FastMCP): 98 | # pass 99 | -------------------------------------------------------------------------------- /tests/server/test_tool_exclude_args.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | import pytest 4 | from mcp.types import TextContent 5 | 6 | from fastmcp import Client, FastMCP 7 | 8 | 9 | async def test_tool_exclude_args_in_tool_manager(): 10 | """Test that tool args are excluded in the tool manager.""" 11 | mcp = FastMCP("Test Server") 12 | 13 | @mcp.tool(exclude_args=["state"]) 14 | def echo(message: str, state: dict[str, Any] | None = None) -> str: 15 | """Echo back the message provided.""" 16 | if state: 17 | # State was read 18 | pass 19 | return message 20 | 21 | tools = mcp._tool_manager.list_tools() 22 | assert len(tools) == 1 23 | assert tools[0].exclude_args is not None 24 | for args in tools[0].exclude_args: 25 | assert args not in tools[0].parameters 26 | 27 | 28 | async def test_tool_exclude_args_without_default_value_raises_error(): 29 | """Test that excluding args without default values raises ValueError""" 30 | mcp = FastMCP("Test Server") 31 | 32 | with pytest.raises(ValueError): 33 | 34 | @mcp.tool(exclude_args=["state"]) 35 | def echo(message: str, state: dict[str, Any] | None) -> str: 36 | """Echo back the message provided.""" 37 | if state: 38 | # State was read 39 | pass 40 | return message 41 | 42 | 43 | async def test_add_tool_method_exclude_args(): 44 | """Test that tool exclude_args work with the add_tool method.""" 45 | mcp = FastMCP("Test Server") 46 | 47 | def create_item( 48 | name: str, value: int, state: dict[str, Any] | None = None 49 | ) -> dict[str, Any]: 50 | """Create a new item.""" 51 | if state: 52 | # State was read 53 | pass 54 | return {"name": name, "value": value} 55 | 56 | mcp.add_tool(create_item, name="create_item", exclude_args=["state"]) 57 | 58 | # Check internal tool objects directly 59 | tools = mcp._tool_manager.list_tools() 60 | assert len(tools) == 1 61 | assert tools[0].exclude_args is not None 62 | assert tools[0].exclude_args == ["state"] 63 | for args in tools[0].exclude_args: 64 | assert args not in tools[0].parameters 65 | 66 | 67 | async def test_tool_functionality_with_exclude_args(): 68 | """Test that tool functionality is preserved when using exclude_args.""" 69 | mcp = FastMCP("Test Server") 70 | 71 | def create_item( 72 | name: str, value: int, state: dict[str, Any] | None = None 73 | ) -> dict[str, Any]: 74 | """Create a new item.""" 75 | if state: 76 | # state was read 77 | pass 78 | return {"name": name, "value": value} 79 | 80 | mcp.add_tool(create_item, name="create_item", exclude_args=["state"]) 81 | 82 | # Use the tool to verify functionality is preserved 83 | async with Client(mcp) as client: 84 | result = await client.call_tool( 85 | "create_item", {"name": "test_item", "value": 42} 86 | ) 87 | assert len(result) == 1 88 | assert isinstance(result[0], TextContent) 89 | 90 | # The result should contain the expected JSON 91 | assert '"name": "test_item"' in result[0].text 92 | assert '"value": 42' in result[0].text 93 | -------------------------------------------------------------------------------- /tests/test_examples.py: -------------------------------------------------------------------------------- 1 | """Tests for example servers""" 2 | 3 | from pydantic import AnyUrl 4 | 5 | from fastmcp import Client 6 | 7 | 8 | async def test_simple_echo(): 9 | """Test the simple echo server""" 10 | from examples.simple_echo import mcp 11 | 12 | async with Client(mcp) as client: 13 | result = await client.call_tool("echo", {"text": "hello"}) 14 | assert len(result) == 1 15 | assert result[0].text == "hello" # type: ignore[attr-defined] 16 | 17 | 18 | async def test_complex_inputs(): 19 | """Test the complex inputs server""" 20 | from examples.complex_inputs import mcp 21 | 22 | async with Client(mcp) as client: 23 | tank = {"shrimp": [{"name": "bob"}, {"name": "alice"}]} 24 | result = await client.call_tool( 25 | "name_shrimp", {"tank": tank, "extra_names": ["charlie"]} 26 | ) 27 | assert len(result) == 1 28 | assert result[0].text == '[\n "bob",\n "alice",\n "charlie"\n]' # type: ignore[attr-defined] 29 | 30 | 31 | async def test_desktop(monkeypatch): 32 | """Test the desktop server""" 33 | from examples.desktop import mcp 34 | 35 | async with Client(mcp) as client: 36 | # Test the add function 37 | result = await client.call_tool("add", {"a": 1, "b": 2}) 38 | assert len(result) == 1 39 | assert result[0].text == "3" # type: ignore[attr-defined] 40 | 41 | async with Client(mcp) as client: 42 | result = await client.read_resource(AnyUrl("greeting://rooter12")) 43 | assert len(result) == 1 44 | assert result[0].text == "Hello, rooter12!" # type: ignore[attr-defined] 45 | 46 | 47 | async def test_echo(): 48 | """Test the echo server""" 49 | from examples.echo import mcp 50 | 51 | async with Client(mcp) as client: 52 | result = await client.call_tool("echo_tool", {"text": "hello"}) 53 | assert len(result) == 1 54 | assert result[0].text == "hello" # type: ignore[attr-defined] 55 | 56 | async with Client(mcp) as client: 57 | result = await client.read_resource(AnyUrl("echo://static")) 58 | assert len(result) == 1 59 | assert result[0].text == "Echo!" # type: ignore[attr-defined] 60 | 61 | async with Client(mcp) as client: 62 | result = await client.read_resource(AnyUrl("echo://server42")) 63 | assert len(result) == 1 64 | assert result[0].text == "Echo: server42" # type: ignore[attr-defined] 65 | 66 | async with Client(mcp) as client: 67 | result = await client.get_prompt("echo", {"text": "hello"}) 68 | assert len(result.messages) == 1 69 | assert result.messages[0].content.text == "hello" # type: ignore[attr-defined] 70 | -------------------------------------------------------------------------------- /tests/test_servers/fastmcp_server.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from fastmcp import FastMCP 4 | 5 | USERS = [ 6 | {"id": "1", "name": "Alice", "active": True}, 7 | {"id": "2", "name": "Bob", "active": True}, 8 | {"id": "3", "name": "Charlie", "active": False}, 9 | ] 10 | 11 | 12 | server = FastMCP("TestServer") 13 | 14 | # --- Tools --- 15 | 16 | 17 | @server.tool() 18 | def greet(name: str) -> str: 19 | """Greet someone by name.""" 20 | return f"Hello, {name}!" 21 | 22 | 23 | @server.tool() 24 | def add(a: int, b: int) -> int: 25 | """Add two numbers together.""" 26 | return a + b 27 | 28 | 29 | @server.tool() 30 | def error_tool(): 31 | """This tool always raises an error.""" 32 | raise ValueError("This is a test error") 33 | 34 | 35 | # --- Resources --- 36 | 37 | 38 | @server.resource(uri="resource://wave") 39 | def wave() -> str: 40 | return "👋" 41 | 42 | 43 | @server.resource(uri="data://users") 44 | async def get_users() -> list[dict[str, Any]]: 45 | return USERS 46 | 47 | 48 | @server.resource(uri="data://user/{user_id}") 49 | async def get_user(user_id: str) -> dict[str, Any] | None: 50 | return next((user for user in USERS if user["id"] == user_id), None) 51 | 52 | 53 | # --- Prompts --- 54 | 55 | 56 | @server.prompt() 57 | def welcome(name: str) -> str: 58 | return f"Welcome to FastMCP, {name}!" 59 | -------------------------------------------------------------------------------- /tests/test_servers/sse.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import fastmcp_server 4 | 5 | if __name__ == "__main__": 6 | asyncio.run(fastmcp_server.server.run_sse_async()) 7 | -------------------------------------------------------------------------------- /tests/test_servers/stdio.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import fastmcp_server 4 | 5 | if __name__ == "__main__": 6 | asyncio.run(fastmcp_server.server.run_stdio_async()) 7 | -------------------------------------------------------------------------------- /tests/tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlowin/fastmcp/97ef80c62478d0f366d62baf3e15875937cd7b9f/tests/tools/__init__.py -------------------------------------------------------------------------------- /tests/utilities/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests for utilities in the fastmcp package.""" 2 | -------------------------------------------------------------------------------- /tests/utilities/openapi/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests for the OpenAPI utilities.""" 2 | -------------------------------------------------------------------------------- /tests/utilities/openapi/conftest.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/utilities/test_logging.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from fastmcp.utilities.logging import get_logger 4 | 5 | 6 | def test_logging_doesnt_affect_other_loggers(caplog): 7 | # set FastMCP loggers to CRITICAL and ensure other loggers still emit messages 8 | original_level = logging.getLogger("FastMCP").getEffectiveLevel() 9 | 10 | try: 11 | logging.getLogger("FastMCP").setLevel(logging.CRITICAL) 12 | 13 | root_logger = logging.getLogger() 14 | app_logger = logging.getLogger("app") 15 | fastmcp_logger = logging.getLogger("FastMCP") 16 | fastmcp_server_logger = get_logger("server") 17 | 18 | with caplog.at_level(logging.INFO): 19 | root_logger.info("--ROOT--") 20 | app_logger.info("--APP--") 21 | fastmcp_logger.info("--FASTMCP--") 22 | fastmcp_server_logger.info("--FASTMCP SERVER--") 23 | 24 | assert "--ROOT--" in caplog.text 25 | assert "--APP--" in caplog.text 26 | assert "--FASTMCP--" not in caplog.text 27 | assert "--FASTMCP SERVER--" not in caplog.text 28 | 29 | finally: 30 | logging.getLogger("FastMCP").setLevel(original_level) 31 | -------------------------------------------------------------------------------- /tests/utilities/test_mcp_config.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from pathlib import Path 3 | 4 | from fastmcp.client.client import Client 5 | from fastmcp.client.transports import ( 6 | SSETransport, 7 | StdioTransport, 8 | StreamableHttpTransport, 9 | ) 10 | from fastmcp.utilities.mcp_config import MCPConfig, RemoteMCPServer, StdioMCPServer 11 | 12 | 13 | def test_parse_single_stdio_config(): 14 | config = { 15 | "mcpServers": { 16 | "test_server": { 17 | "command": "echo", 18 | "args": ["hello"], 19 | } 20 | } 21 | } 22 | mcp_config = MCPConfig.from_dict(config) 23 | transport = mcp_config.mcpServers["test_server"].to_transport() 24 | assert isinstance(transport, StdioTransport) 25 | assert transport.command == "echo" 26 | assert transport.args == ["hello"] 27 | 28 | 29 | def test_parse_single_remote_config(): 30 | config = { 31 | "mcpServers": { 32 | "test_server": { 33 | "url": "http://localhost:8000", 34 | } 35 | } 36 | } 37 | mcp_config = MCPConfig.from_dict(config) 38 | transport = mcp_config.mcpServers["test_server"].to_transport() 39 | assert isinstance(transport, StreamableHttpTransport) 40 | assert transport.url == "http://localhost:8000" 41 | 42 | 43 | def test_parse_remote_config_with_transport(): 44 | config = { 45 | "mcpServers": { 46 | "test_server": { 47 | "url": "http://localhost:8000", 48 | "transport": "sse", 49 | } 50 | } 51 | } 52 | mcp_config = MCPConfig.from_dict(config) 53 | transport = mcp_config.mcpServers["test_server"].to_transport() 54 | assert isinstance(transport, SSETransport) 55 | assert transport.url == "http://localhost:8000" 56 | 57 | 58 | def test_parse_remote_config_with_url_inference(): 59 | config = { 60 | "mcpServers": { 61 | "test_server": { 62 | "url": "http://localhost:8000/sse", 63 | } 64 | } 65 | } 66 | mcp_config = MCPConfig.from_dict(config) 67 | transport = mcp_config.mcpServers["test_server"].to_transport() 68 | assert isinstance(transport, SSETransport) 69 | assert transport.url == "http://localhost:8000/sse" 70 | 71 | 72 | def test_parse_multiple_servers(): 73 | config = { 74 | "mcpServers": { 75 | "test_server": { 76 | "url": "http://localhost:8000/sse", 77 | }, 78 | "test_server_2": { 79 | "command": "echo", 80 | "args": ["hello"], 81 | "env": {"TEST": "test"}, 82 | }, 83 | } 84 | } 85 | mcp_config = MCPConfig.from_dict(config) 86 | assert len(mcp_config.mcpServers) == 2 87 | assert isinstance(mcp_config.mcpServers["test_server"], RemoteMCPServer) 88 | assert isinstance(mcp_config.mcpServers["test_server"].to_transport(), SSETransport) 89 | 90 | assert isinstance(mcp_config.mcpServers["test_server_2"], StdioMCPServer) 91 | assert isinstance( 92 | mcp_config.mcpServers["test_server_2"].to_transport(), StdioTransport 93 | ) 94 | assert mcp_config.mcpServers["test_server_2"].command == "echo" 95 | assert mcp_config.mcpServers["test_server_2"].args == ["hello"] 96 | assert mcp_config.mcpServers["test_server_2"].env == {"TEST": "test"} 97 | 98 | 99 | async def test_multi_client(tmp_path: Path): 100 | server_script = inspect.cleandoc(""" 101 | from fastmcp import FastMCP 102 | 103 | mcp = FastMCP() 104 | 105 | @mcp.tool() 106 | def add(a: int, b: int) -> int: 107 | return a + b 108 | 109 | if __name__ == '__main__': 110 | mcp.run() 111 | """) 112 | 113 | script_path = tmp_path / "test.py" 114 | script_path.write_text(server_script) 115 | 116 | config = { 117 | "mcpServers": { 118 | "test_1": { 119 | "command": "python", 120 | "args": [str(script_path)], 121 | }, 122 | "test_2": { 123 | "command": "python", 124 | "args": [str(script_path)], 125 | }, 126 | } 127 | } 128 | 129 | client = Client(config) 130 | 131 | async with client: 132 | tools = await client.list_tools() 133 | assert len(tools) == 2 134 | 135 | result_1 = await client.call_tool("test_1_add", {"a": 1, "b": 2}) 136 | result_2 = await client.call_tool("test_2_add", {"a": 1, "b": 2}) 137 | assert result_1[0].text == "3" # type: ignore[attr-dict] 138 | assert result_2[0].text == "3" # type: ignore[attr-dict] 139 | -------------------------------------------------------------------------------- /tests/utilities/test_tests.py: -------------------------------------------------------------------------------- 1 | import fastmcp 2 | from fastmcp.utilities.tests import temporary_settings 3 | 4 | 5 | class TestTemporarySettings: 6 | def test_temporary_settings(self): 7 | assert fastmcp.settings.settings.log_level == "DEBUG" 8 | with temporary_settings(log_level="ERROR"): 9 | assert fastmcp.settings.settings.log_level == "ERROR" 10 | assert fastmcp.settings.settings.log_level == "DEBUG" 11 | --------------------------------------------------------------------------------