├── .git-blame-ignore-revs ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── check-lock.yml │ ├── main-checks.yml │ ├── publish-docs-manually.yml │ ├── publish-pypi.yml │ ├── pull-request-checks.yml │ └── shared.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CLAUDE.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── RELEASE.md ├── SECURITY.md ├── docs ├── api.md └── index.md ├── examples ├── README.md ├── clients │ └── simple-chatbot │ │ ├── .python-version │ │ ├── README.MD │ │ ├── mcp_simple_chatbot │ │ ├── .env.example │ │ ├── main.py │ │ ├── requirements.txt │ │ ├── servers_config.json │ │ └── test.db │ │ ├── pyproject.toml │ │ └── uv.lock ├── fastmcp │ ├── complex_inputs.py │ ├── desktop.py │ ├── echo.py │ ├── memory.py │ ├── parameter_descriptions.py │ ├── readme-quickstart.py │ ├── screenshot.py │ ├── simple_echo.py │ ├── text_me.py │ └── unicode_example.py └── servers │ ├── simple-prompt │ ├── .python-version │ ├── README.md │ ├── mcp_simple_prompt │ │ ├── __init__.py │ │ ├── __main__.py │ │ └── server.py │ └── pyproject.toml │ ├── simple-resource │ ├── .python-version │ ├── README.md │ ├── mcp_simple_resource │ │ ├── __init__.py │ │ ├── __main__.py │ │ └── server.py │ └── pyproject.toml │ └── simple-tool │ ├── .python-version │ ├── README.md │ ├── mcp_simple_tool │ ├── __init__.py │ ├── __main__.py │ └── server.py │ └── pyproject.toml ├── mkdocs.yml ├── pyproject.toml ├── src └── mcp │ ├── __init__.py │ ├── cli │ ├── __init__.py │ ├── claude.py │ └── cli.py │ ├── client │ ├── __init__.py │ ├── __main__.py │ ├── session.py │ ├── sse.py │ ├── stdio │ │ ├── __init__.py │ │ └── win32.py │ └── websocket.py │ ├── py.typed │ ├── server │ ├── __init__.py │ ├── __main__.py │ ├── fastmcp │ │ ├── __init__.py │ │ ├── exceptions.py │ │ ├── prompts │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── manager.py │ │ │ └── prompt_manager.py │ │ ├── resources │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── resource_manager.py │ │ │ ├── templates.py │ │ │ └── types.py │ │ ├── server.py │ │ ├── tools │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ └── tool_manager.py │ │ └── utilities │ │ │ ├── __init__.py │ │ │ ├── func_metadata.py │ │ │ ├── logging.py │ │ │ └── types.py │ ├── lowlevel │ │ ├── __init__.py │ │ ├── helper_types.py │ │ └── server.py │ ├── models.py │ ├── session.py │ ├── sse.py │ ├── stdio.py │ └── websocket.py │ ├── shared │ ├── __init__.py │ ├── context.py │ ├── exceptions.py │ ├── memory.py │ ├── progress.py │ ├── session.py │ └── version.py │ └── types.py ├── tests ├── __init__.py ├── client │ ├── __init__.py │ ├── test_config.py │ ├── test_list_roots_callback.py │ ├── test_logging_callback.py │ ├── test_sampling_callback.py │ ├── test_session.py │ └── test_stdio.py ├── conftest.py ├── issues │ ├── test_100_tool_listing.py │ ├── test_129_resource_templates.py │ ├── test_141_resource_templates.py │ ├── test_152_resource_mime_type.py │ ├── test_176_progress_token.py │ ├── test_188_concurrency.py │ ├── test_192_request_id.py │ ├── test_342_base64_encoding.py │ ├── test_355_type_error.py │ └── test_88_random_error.py ├── server │ ├── __init__.py │ ├── fastmcp │ │ ├── __init__.py │ │ ├── prompts │ │ │ ├── __init__.py │ │ │ ├── test_base.py │ │ │ └── test_manager.py │ │ ├── resources │ │ │ ├── __init__.py │ │ │ ├── test_file_resources.py │ │ │ ├── test_function_resources.py │ │ │ ├── test_resource_manager.py │ │ │ ├── test_resource_template.py │ │ │ └── test_resources.py │ │ ├── servers │ │ │ ├── __init__.py │ │ │ └── test_file_server.py │ │ ├── test_func_metadata.py │ │ ├── test_parameter_descriptions.py │ │ ├── test_server.py │ │ └── test_tool_manager.py │ ├── test_lifespan.py │ ├── test_read_resource.py │ ├── test_session.py │ └── test_stdio.py ├── shared │ ├── test_memory.py │ ├── test_session.py │ ├── test_sse.py │ └── test_ws.py ├── test_examples.py └── test_types.py └── uv.lock /.git-blame-ignore-revs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/b4c7db6a50a5c88bae1db5c1f7fba44d16eebc6e/.git-blame-ignore-revs -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/check-lock.yml: -------------------------------------------------------------------------------- 1 | name: Check uv.lock 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - "pyproject.toml" 7 | - "uv.lock" 8 | push: 9 | paths: 10 | - "pyproject.toml" 11 | - "uv.lock" 12 | 13 | jobs: 14 | check-lock: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Install uv 20 | run: | 21 | curl -LsSf https://astral.sh/uv/install.sh | sh 22 | echo "$HOME/.cargo/bin" >> $GITHUB_PATH 23 | 24 | - name: Check uv.lock is up to date 25 | run: uv lock --check 26 | -------------------------------------------------------------------------------- /.github/workflows/main-checks.yml: -------------------------------------------------------------------------------- 1 | name: Main branch checks 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - "v*.*.*" 8 | tags: 9 | - "v*.*.*" 10 | 11 | jobs: 12 | checks: 13 | uses: ./.github/workflows/shared.yml 14 | -------------------------------------------------------------------------------- /.github/workflows/publish-docs-manually.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docs manually 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | docs-publish: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | contents: write 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Configure Git Credentials 14 | run: | 15 | git config user.name github-actions[bot] 16 | git config user.email 41898282+github-actions[bot]@users.noreply.github.com 17 | 18 | - name: Install uv 19 | uses: astral-sh/setup-uv@v3 20 | with: 21 | enable-cache: true 22 | 23 | - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV 24 | - uses: actions/cache@v4 25 | with: 26 | key: mkdocs-material-${{ env.cache_id }} 27 | path: .cache 28 | restore-keys: | 29 | mkdocs-material- 30 | 31 | - run: uv sync --frozen --group docs 32 | - run: uv run --no-sync mkdocs gh-deploy --force 33 | -------------------------------------------------------------------------------- /.github/workflows/publish-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publishing 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | release-build: 9 | name: Build distribution 10 | runs-on: ubuntu-latest 11 | needs: [checks] 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Install uv 16 | uses: astral-sh/setup-uv@v3 17 | with: 18 | enable-cache: true 19 | 20 | - name: Set up Python 3.12 21 | run: uv python install 3.12 22 | 23 | - name: Build 24 | run: uv build 25 | 26 | - name: Upload artifacts 27 | uses: actions/upload-artifact@v4 28 | with: 29 | name: release-dists 30 | path: dist/ 31 | 32 | checks: 33 | uses: ./.github/workflows/shared.yml 34 | 35 | pypi-publish: 36 | name: Upload release to PyPI 37 | runs-on: ubuntu-latest 38 | environment: release 39 | needs: 40 | - release-build 41 | permissions: 42 | id-token: write # IMPORTANT: this permission is mandatory for trusted publishing 43 | 44 | steps: 45 | - name: Retrieve release distributions 46 | uses: actions/download-artifact@v4 47 | with: 48 | name: release-dists 49 | path: dist/ 50 | 51 | - name: Publish package distributions to PyPI 52 | uses: pypa/gh-action-pypi-publish@release/v1 53 | 54 | docs-publish: 55 | runs-on: ubuntu-latest 56 | needs: ["pypi-publish"] 57 | permissions: 58 | contents: write 59 | steps: 60 | - uses: actions/checkout@v4 61 | - name: Configure Git Credentials 62 | run: | 63 | git config user.name github-actions[bot] 64 | git config user.email 41898282+github-actions[bot]@users.noreply.github.com 65 | 66 | - name: Install uv 67 | uses: astral-sh/setup-uv@v3 68 | with: 69 | enable-cache: true 70 | 71 | - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV 72 | - uses: actions/cache@v4 73 | with: 74 | key: mkdocs-material-${{ env.cache_id }} 75 | path: .cache 76 | restore-keys: | 77 | mkdocs-material- 78 | 79 | - run: uv sync --frozen --group docs 80 | - run: uv run --no-sync mkdocs gh-deploy --force 81 | -------------------------------------------------------------------------------- /.github/workflows/pull-request-checks.yml: -------------------------------------------------------------------------------- 1 | name: Pull request checks 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | checks: 8 | uses: ./.github/workflows/shared.yml 9 | -------------------------------------------------------------------------------- /.github/workflows/shared.yml: -------------------------------------------------------------------------------- 1 | name: Shared Checks 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | format: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | 12 | - name: Install uv 13 | uses: astral-sh/setup-uv@v3 14 | with: 15 | enable-cache: true 16 | 17 | - name: Install the project 18 | run: uv sync --frozen --all-extras --dev --python 3.12 19 | 20 | - name: Run ruff format check 21 | run: uv run --no-sync ruff check . 22 | 23 | typecheck: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v4 27 | 28 | - name: Install uv 29 | uses: astral-sh/setup-uv@v3 30 | with: 31 | enable-cache: true 32 | 33 | - name: Install the project 34 | run: uv sync --frozen --all-extras --dev --python 3.12 35 | 36 | - name: Run pyright 37 | run: uv run --no-sync pyright 38 | 39 | test: 40 | runs-on: ubuntu-latest 41 | strategy: 42 | matrix: 43 | python-version: ["3.10", "3.11", "3.12", "3.13"] 44 | 45 | steps: 46 | - uses: actions/checkout@v4 47 | 48 | - name: Install uv 49 | uses: astral-sh/setup-uv@v3 50 | with: 51 | enable-cache: true 52 | 53 | - name: Install the project 54 | run: uv sync --frozen --all-extras --dev --python ${{ matrix.python-version }} 55 | 56 | - name: Run pytest 57 | run: uv run --no-sync pytest 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | scratch/ 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | cover/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | .pybuilder/ 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | # For a library or package, you might want to ignore these files since the code is 90 | # intended to run in multiple environments; otherwise, check them in: 91 | # .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # poetry 101 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 102 | # This is especially recommended for binary packages to ensure reproducibility, and is more 103 | # commonly ignored for libraries. 104 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 105 | #poetry.lock 106 | 107 | # pdm 108 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 109 | #pdm.lock 110 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 111 | # in version control. 112 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 113 | .pdm.toml 114 | .pdm-python 115 | .pdm-build/ 116 | 117 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 118 | __pypackages__/ 119 | 120 | # Celery stuff 121 | celerybeat-schedule 122 | celerybeat.pid 123 | 124 | # SageMath parsed files 125 | *.sage.py 126 | 127 | # Environments 128 | .env 129 | .venv 130 | env/ 131 | venv/ 132 | ENV/ 133 | env.bak/ 134 | venv.bak/ 135 | 136 | # Spyder project settings 137 | .spyderproject 138 | .spyproject 139 | 140 | # Rope project settings 141 | .ropeproject 142 | 143 | # mkdocs documentation 144 | /site 145 | 146 | # mypy 147 | .mypy_cache/ 148 | .dmypy.json 149 | dmypy.json 150 | 151 | # Pyre type checker 152 | .pyre/ 153 | 154 | # pytype static type analyzer 155 | .pytype/ 156 | 157 | # Cython debug symbols 158 | cython_debug/ 159 | 160 | # PyCharm 161 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 162 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 163 | # and can be added to the global gitignore or merged into this file. For a more nuclear 164 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 165 | #.idea/ 166 | 167 | # vscode 168 | .vscode/ 169 | **/CLAUDE.local.md 170 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | fail_fast: true 2 | 3 | repos: 4 | - repo: https://github.com/pre-commit/mirrors-prettier 5 | rev: v3.1.0 6 | hooks: 7 | - id: prettier 8 | types_or: [yaml, json5] 9 | 10 | - repo: local 11 | hooks: 12 | - id: ruff-format 13 | name: Ruff Format 14 | entry: uv run ruff 15 | args: [format] 16 | language: system 17 | types: [python] 18 | pass_filenames: false 19 | - id: ruff 20 | name: Ruff 21 | entry: uv run ruff 22 | args: ["check", "--fix", "--exit-non-zero-on-fix"] 23 | types: [python] 24 | language: system 25 | pass_filenames: false 26 | - id: pyright 27 | name: pyright 28 | entry: uv run pyright 29 | args: [src] 30 | language: system 31 | types: [python] 32 | pass_filenames: false 33 | - id: uv-lock-check 34 | name: Check uv.lock is up to date 35 | entry: uv lock --check 36 | language: system 37 | files: ^(pyproject\.toml|uv\.lock)$ 38 | pass_filenames: false 39 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # Development Guidelines 2 | 3 | This document contains critical information about working with this codebase. Follow these guidelines precisely. 4 | 5 | ## Core Development Rules 6 | 7 | 1. Package Management 8 | - ONLY use uv, NEVER pip 9 | - Installation: `uv add package` 10 | - Running tools: `uv run tool` 11 | - Upgrading: `uv add --dev package --upgrade-package package` 12 | - FORBIDDEN: `uv pip install`, `@latest` syntax 13 | 14 | 2. Code Quality 15 | - Type hints required for all code 16 | - Public APIs must have docstrings 17 | - Functions must be focused and small 18 | - Follow existing patterns exactly 19 | - Line length: 88 chars maximum 20 | 21 | 3. Testing Requirements 22 | - Framework: `uv run pytest` 23 | - Async testing: use anyio, not asyncio 24 | - Coverage: test edge cases and errors 25 | - New features require tests 26 | - Bug fixes require regression tests 27 | 28 | - For commits fixing bugs or adding features based on user reports add: 29 | ```bash 30 | git commit --trailer "Reported-by:" 31 | ``` 32 | Where `` is the name of the user. 33 | 34 | - For commits related to a Github issue, add 35 | ```bash 36 | git commit --trailer "Github-Issue:#" 37 | ``` 38 | - NEVER ever mention a `co-authored-by` or similar aspects. In particular, never 39 | mention the tool used to create the commit message or PR. 40 | 41 | ## Pull Requests 42 | 43 | - Create a detailed message of what changed. Focus on the high level description of 44 | the problem it tries to solve, and how it is solved. Don't go into the specifics of the 45 | code unless it adds clarity. 46 | 47 | - Always add `jerome3o-anthropic` and `jspahrsummers` as reviewer. 48 | 49 | - NEVER ever mention a `co-authored-by` or similar aspects. In particular, never 50 | mention the tool used to create the commit message or PR. 51 | 52 | ## Python Tools 53 | 54 | ## Code Formatting 55 | 56 | 1. Ruff 57 | - Format: `uv run ruff format .` 58 | - Check: `uv run ruff check .` 59 | - Fix: `uv run ruff check . --fix` 60 | - Critical issues: 61 | - Line length (88 chars) 62 | - Import sorting (I001) 63 | - Unused imports 64 | - Line wrapping: 65 | - Strings: use parentheses 66 | - Function calls: multi-line with proper indent 67 | - Imports: split into multiple lines 68 | 69 | 2. Type Checking 70 | - Tool: `uv run pyright` 71 | - Requirements: 72 | - Explicit None checks for Optional 73 | - Type narrowing for strings 74 | - Version warnings can be ignored if checks pass 75 | 76 | 3. Pre-commit 77 | - Config: `.pre-commit-config.yaml` 78 | - Runs: on git commit 79 | - Tools: Prettier (YAML/JSON), Ruff (Python) 80 | - Ruff updates: 81 | - Check PyPI versions 82 | - Update config rev 83 | - Commit config first 84 | 85 | ## Error Resolution 86 | 87 | 1. CI Failures 88 | - Fix order: 89 | 1. Formatting 90 | 2. Type errors 91 | 3. Linting 92 | - Type errors: 93 | - Get full line context 94 | - Check Optional types 95 | - Add type narrowing 96 | - Verify function signatures 97 | 98 | 2. Common Issues 99 | - Line length: 100 | - Break strings with parentheses 101 | - Multi-line function calls 102 | - Split imports 103 | - Types: 104 | - Add None checks 105 | - Narrow string types 106 | - Match existing patterns 107 | 108 | 3. Best Practices 109 | - Check git status before commits 110 | - Run formatters before type checks 111 | - Keep changes minimal 112 | - Follow existing patterns 113 | - Document public APIs 114 | - Test thoroughly 115 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | mcp-coc@anthropic.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for your interest in contributing to the MCP Python SDK! This document provides guidelines and instructions for contributing. 4 | 5 | ## Development Setup 6 | 7 | 1. Make sure you have Python 3.10+ installed 8 | 2. Install [uv](https://docs.astral.sh/uv/getting-started/installation/) 9 | 3. Fork the repository 10 | 4. Clone your fork: `git clone https://github.com/YOUR-USERNAME/python-sdk.git` 11 | 5. Install dependencies: 12 | ```bash 13 | uv sync --frozen --all-extras --dev 14 | ``` 15 | 16 | ## Development Workflow 17 | 18 | 1. Choose the correct branch for your changes: 19 | - For bug fixes to a released version: use the latest release branch (e.g. v1.1.x for 1.1.3) 20 | - For new features: use the main branch (which will become the next minor/major version) 21 | - If unsure, ask in an issue first 22 | 23 | 2. Create a new branch from your chosen base branch 24 | 25 | 3. Make your changes 26 | 27 | 4. Ensure tests pass: 28 | ```bash 29 | uv run pytest 30 | ``` 31 | 32 | 5. Run type checking: 33 | ```bash 34 | uv run pyright 35 | ``` 36 | 37 | 6. Run linting: 38 | ```bash 39 | uv run ruff check . 40 | uv run ruff format . 41 | ``` 42 | 43 | 7. Submit a pull request to the same branch you branched from 44 | 45 | ## Code Style 46 | 47 | - We use `ruff` for linting and formatting 48 | - Follow PEP 8 style guidelines 49 | - Add type hints to all functions 50 | - Include docstrings for public APIs 51 | 52 | ## Pull Request Process 53 | 54 | 1. Update documentation as needed 55 | 2. Add tests for new functionality 56 | 3. Ensure CI passes 57 | 4. Maintainers will review your code 58 | 5. Address review feedback 59 | 60 | ## Code of Conduct 61 | 62 | Please note that this project is released with a [Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms. 63 | 64 | ## License 65 | 66 | By contributing, you agree that your contributions will be licensed under the MIT License. 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Anthropic, PBC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release Process 2 | 3 | ## Bumping Dependencies 4 | 5 | 1. Change dependency version in `pyproject.toml` 6 | 2. Upgrade lock with `uv lock --resolution lowest-direct` 7 | 8 | ## Major or Minor Release 9 | 10 | Create a GitHub release via UI with the tag being `vX.Y.Z` where `X.Y.Z` is the version, 11 | and the release title being the same. Then ask someone to review the release. 12 | 13 | The package version will be set automatically from the tag. 14 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | Thank you for helping us keep the SDKs and systems they interact with secure. 3 | 4 | ## Reporting Security Issues 5 | 6 | This SDK is maintained by [Anthropic](https://www.anthropic.com/) as part of the Model Context Protocol project. 7 | 8 | The security of our systems and user data is Anthropic’s top priority. We appreciate the work of security researchers acting in good faith in identifying and reporting potential vulnerabilities. 9 | 10 | Our security program is managed on HackerOne and we ask that any validated vulnerability in this functionality be reported through their [submission form](https://hackerone.com/anthropic-vdp/reports/new?type=team&report_type=vulnerability). 11 | 12 | ## Vulnerability Disclosure Program 13 | 14 | Our Vulnerability Program Guidelines are defined on our [HackerOne program page](https://hackerone.com/anthropic-vdp). 15 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | ::: mcp 2 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # MCP Server 2 | 3 | This is the MCP Server implementation in Python. 4 | 5 | It only contains the [API Reference](api.md) for the time being. 6 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Python SDK Examples 2 | 3 | This folders aims to provide simple examples of using the Python SDK. Please refer to the 4 | [servers repository](https://github.com/modelcontextprotocol/servers) 5 | for real-world servers. 6 | -------------------------------------------------------------------------------- /examples/clients/simple-chatbot/.python-version: -------------------------------------------------------------------------------- 1 | 3.10 2 | -------------------------------------------------------------------------------- /examples/clients/simple-chatbot/README.MD: -------------------------------------------------------------------------------- 1 | # MCP Simple Chatbot 2 | 3 | This example demonstrates how to integrate the Model Context Protocol (MCP) into a simple CLI chatbot. The implementation showcases MCP's flexibility by supporting multiple tools through MCP servers and is compatible with any LLM provider that follows OpenAI API standards. 4 | 5 | ## Requirements 6 | 7 | - Python 3.10 8 | - `python-dotenv` 9 | - `requests` 10 | - `mcp` 11 | - `uvicorn` 12 | 13 | ## Installation 14 | 15 | 1. **Install the dependencies:** 16 | 17 | ```bash 18 | pip install -r requirements.txt 19 | ``` 20 | 21 | 2. **Set up environment variables:** 22 | 23 | Create a `.env` file in the root directory and add your API key: 24 | 25 | ```plaintext 26 | LLM_API_KEY=your_api_key_here 27 | ``` 28 | 29 | 3. **Configure servers:** 30 | 31 | The `servers_config.json` follows the same structure as Claude Desktop, allowing for easy integration of multiple servers. 32 | Here's an example: 33 | 34 | ```json 35 | { 36 | "mcpServers": { 37 | "sqlite": { 38 | "command": "uvx", 39 | "args": ["mcp-server-sqlite", "--db-path", "./test.db"] 40 | }, 41 | "puppeteer": { 42 | "command": "npx", 43 | "args": ["-y", "@modelcontextprotocol/server-puppeteer"] 44 | } 45 | } 46 | } 47 | ``` 48 | Environment variables are supported as well. Pass them as you would with the Claude Desktop App. 49 | 50 | Example: 51 | ```json 52 | { 53 | "mcpServers": { 54 | "server_name": { 55 | "command": "uvx", 56 | "args": ["mcp-server-name", "--additional-args"], 57 | "env": { 58 | "API_KEY": "your_api_key_here" 59 | } 60 | } 61 | } 62 | } 63 | ``` 64 | 65 | ## Usage 66 | 67 | 1. **Run the client:** 68 | 69 | ```bash 70 | python main.py 71 | ``` 72 | 73 | 2. **Interact with the assistant:** 74 | 75 | The assistant will automatically detect available tools and can respond to queries based on the tools provided by the configured servers. 76 | 77 | 3. **Exit the session:** 78 | 79 | Type `quit` or `exit` to end the session. 80 | 81 | ## Architecture 82 | 83 | - **Tool Discovery**: Tools are automatically discovered from configured servers. 84 | - **System Prompt**: Tools are dynamically included in the system prompt, allowing the LLM to understand available capabilities. 85 | - **Server Integration**: Supports any MCP-compatible server, tested with various server implementations including Uvicorn and Node.js. 86 | 87 | ### Class Structure 88 | - **Configuration**: Manages environment variables and server configurations 89 | - **Server**: Handles MCP server initialization, tool discovery, and execution 90 | - **Tool**: Represents individual tools with their properties and formatting 91 | - **LLMClient**: Manages communication with the LLM provider 92 | - **ChatSession**: Orchestrates the interaction between user, LLM, and tools 93 | 94 | ### Logic Flow 95 | 96 | 1. **Tool Integration**: 97 | - Tools are dynamically discovered from MCP servers 98 | - Tool descriptions are automatically included in system prompt 99 | - Tool execution is handled through standardized MCP protocol 100 | 101 | 2. **Runtime Flow**: 102 | - User input is received 103 | - Input is sent to LLM with context of available tools 104 | - LLM response is parsed: 105 | - If it's a tool call → execute tool and return result 106 | - If it's a direct response → return to user 107 | - Tool results are sent back to LLM for interpretation 108 | - Final response is presented to user 109 | 110 | 111 | -------------------------------------------------------------------------------- /examples/clients/simple-chatbot/mcp_simple_chatbot/.env.example: -------------------------------------------------------------------------------- 1 | LLM_API_KEY=gsk_1234567890 2 | -------------------------------------------------------------------------------- /examples/clients/simple-chatbot/mcp_simple_chatbot/requirements.txt: -------------------------------------------------------------------------------- 1 | python-dotenv>=1.0.0 2 | requests>=2.31.0 3 | mcp>=1.0.0 4 | uvicorn>=0.32.1 -------------------------------------------------------------------------------- /examples/clients/simple-chatbot/mcp_simple_chatbot/servers_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "sqlite": { 4 | "command": "uvx", 5 | "args": ["mcp-server-sqlite", "--db-path", "./test.db"] 6 | }, 7 | "puppeteer": { 8 | "command": "npx", 9 | "args": ["-y", "@modelcontextprotocol/server-puppeteer"] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /examples/clients/simple-chatbot/mcp_simple_chatbot/test.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/b4c7db6a50a5c88bae1db5c1f7fba44d16eebc6e/examples/clients/simple-chatbot/mcp_simple_chatbot/test.db -------------------------------------------------------------------------------- /examples/clients/simple-chatbot/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "mcp-simple-chatbot" 3 | version = "0.1.0" 4 | description = "A simple CLI chatbot using the Model Context Protocol (MCP)" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | authors = [{ name = "Edoardo Cilia" }] 8 | keywords = ["mcp", "llm", "chatbot", "cli"] 9 | license = { text = "MIT" } 10 | classifiers = [ 11 | "Development Status :: 4 - Beta", 12 | "Intended Audience :: Developers", 13 | "License :: OSI Approved :: MIT License", 14 | "Programming Language :: Python :: 3", 15 | "Programming Language :: Python :: 3.10", 16 | ] 17 | dependencies = [ 18 | "python-dotenv>=1.0.0", 19 | "requests>=2.31.0", 20 | "mcp>=1.0.0", 21 | "uvicorn>=0.32.1" 22 | ] 23 | 24 | [project.scripts] 25 | mcp-simple-chatbot = "mcp_simple_chatbot.client:main" 26 | 27 | [build-system] 28 | requires = ["hatchling"] 29 | build-backend = "hatchling.build" 30 | 31 | [tool.hatch.build.targets.wheel] 32 | packages = ["mcp_simple_chatbot"] 33 | 34 | [tool.pyright] 35 | include = ["mcp_simple_chatbot"] 36 | venvPath = "." 37 | venv = ".venv" 38 | 39 | [tool.ruff.lint] 40 | select = ["E", "F", "I"] 41 | ignore = [] 42 | 43 | [tool.ruff] 44 | line-length = 88 45 | target-version = "py310" 46 | 47 | [tool.uv] 48 | dev-dependencies = ["pyright>=1.1.379", "pytest>=8.3.3", "ruff>=0.6.9"] 49 | -------------------------------------------------------------------------------- /examples/fastmcp/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 mcp.server.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/fastmcp/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 mcp.server.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 | @mcp.tool() 23 | def add(a: int, b: int) -> int: 24 | """Add two numbers""" 25 | return a + b 26 | -------------------------------------------------------------------------------- /examples/fastmcp/echo.py: -------------------------------------------------------------------------------- 1 | """ 2 | FastMCP Echo Server 3 | """ 4 | 5 | from mcp.server.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/fastmcp/parameter_descriptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | FastMCP Example showing parameter descriptions 3 | """ 4 | 5 | from pydantic import Field 6 | 7 | from mcp.server.fastmcp import FastMCP 8 | 9 | # Create server 10 | mcp = FastMCP("Parameter Descriptions Server") 11 | 12 | 13 | @mcp.tool() 14 | def greet_user( 15 | name: str = Field(description="The name of the person to greet"), 16 | title: str = Field(description="Optional title like Mr/Ms/Dr", default=""), 17 | times: int = Field(description="Number of times to repeat the greeting", default=1), 18 | ) -> str: 19 | """Greet a user with optional title and repetition""" 20 | greeting = f"Hello {title + ' ' if title else ''}{name}!" 21 | return "\n".join([greeting] * times) 22 | -------------------------------------------------------------------------------- /examples/fastmcp/readme-quickstart.py: -------------------------------------------------------------------------------- 1 | from mcp.server.fastmcp import FastMCP 2 | 3 | # Create an MCP server 4 | mcp = FastMCP("Demo") 5 | 6 | 7 | # Add an addition tool 8 | @mcp.tool() 9 | def add(a: int, b: int) -> int: 10 | """Add two numbers""" 11 | return a + b 12 | 13 | 14 | # Add a dynamic greeting resource 15 | @mcp.resource("greeting://{name}") 16 | def get_greeting(name: str) -> str: 17 | """Get a personalized greeting""" 18 | return f"Hello, {name}!" 19 | -------------------------------------------------------------------------------- /examples/fastmcp/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 mcp.server.fastmcp import FastMCP 10 | from mcp.server.fastmcp.utilities.types import Image 11 | 12 | # Create server 13 | mcp = FastMCP("Screenshot Demo", dependencies=["pyautogui", "Pillow"]) 14 | 15 | 16 | @mcp.tool() 17 | def take_screenshot() -> Image: 18 | """ 19 | Take a screenshot of the user's screen and return it as an image. Use 20 | this tool anytime the user wants you to look at something they're doing. 21 | """ 22 | import pyautogui 23 | 24 | buffer = io.BytesIO() 25 | 26 | # if the file exceeds ~1MB, it will be rejected by Claude 27 | screenshot = pyautogui.screenshot() 28 | screenshot.convert("RGB").save(buffer, format="JPEG", quality=60, optimize=True) 29 | return Image(data=buffer.getvalue(), format="jpeg") 30 | -------------------------------------------------------------------------------- /examples/fastmcp/simple_echo.py: -------------------------------------------------------------------------------- 1 | """ 2 | FastMCP Echo Server 3 | """ 4 | 5 | from mcp.server.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/fastmcp/text_me.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # dependencies = [] 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 mcp.server.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 | -------------------------------------------------------------------------------- /examples/fastmcp/unicode_example.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example FastMCP server that uses Unicode characters in various places to help test 3 | Unicode handling in tools and inspectors. 4 | """ 5 | 6 | from mcp.server.fastmcp import FastMCP 7 | 8 | mcp = FastMCP() 9 | 10 | 11 | @mcp.tool( 12 | description="🌟 A tool that uses various Unicode characters in its description: " 13 | "á é í ó ú ñ 漢字 🎉" 14 | ) 15 | def hello_unicode(name: str = "世界", greeting: str = "¡Hola") -> str: 16 | """ 17 | A simple tool that demonstrates Unicode handling in: 18 | - Tool description (emojis, accents, CJK characters) 19 | - Parameter defaults (CJK characters) 20 | - Return values (Spanish punctuation, emojis) 21 | """ 22 | return f"{greeting}, {name}! 👋" 23 | 24 | 25 | @mcp.tool(description="🎨 Tool that returns a list of emoji categories") 26 | def list_emoji_categories() -> list[str]: 27 | """Returns a list of emoji categories with emoji examples.""" 28 | return [ 29 | "😀 Smileys & Emotion", 30 | "👋 People & Body", 31 | "🐶 Animals & Nature", 32 | "🍎 Food & Drink", 33 | "⚽ Activities", 34 | "🌍 Travel & Places", 35 | "💡 Objects", 36 | "❤️ Symbols", 37 | "🚩 Flags", 38 | ] 39 | 40 | 41 | @mcp.tool(description="🔤 Tool that returns text in different scripts") 42 | def multilingual_hello() -> str: 43 | """Returns hello in different scripts and writing systems.""" 44 | return "\n".join( 45 | [ 46 | "English: Hello!", 47 | "Spanish: ¡Hola!", 48 | "French: Bonjour!", 49 | "German: Grüß Gott!", 50 | "Russian: Привет!", 51 | "Greek: Γεια σας!", 52 | "Hebrew: !שָׁלוֹם", 53 | "Arabic: !مرحبا", 54 | "Hindi: नमस्ते!", 55 | "Chinese: 你好!", 56 | "Japanese: こんにちは!", 57 | "Korean: 안녕하세요!", 58 | "Thai: สวัสดี!", 59 | ] 60 | ) 61 | 62 | 63 | if __name__ == "__main__": 64 | mcp.run() 65 | -------------------------------------------------------------------------------- /examples/servers/simple-prompt/.python-version: -------------------------------------------------------------------------------- 1 | 3.10 2 | -------------------------------------------------------------------------------- /examples/servers/simple-prompt/README.md: -------------------------------------------------------------------------------- 1 | # MCP Simple Prompt 2 | 3 | A simple MCP server that exposes a customizable prompt template with optional context and topic parameters. 4 | 5 | ## Usage 6 | 7 | Start the server using either stdio (default) or SSE transport: 8 | 9 | ```bash 10 | # Using stdio transport (default) 11 | uv run mcp-simple-prompt 12 | 13 | # Using SSE transport on custom port 14 | uv run mcp-simple-prompt --transport sse --port 8000 15 | ``` 16 | 17 | The server exposes a prompt named "simple" that accepts two optional arguments: 18 | 19 | - `context`: Additional context to consider 20 | - `topic`: Specific topic to focus on 21 | 22 | ## Example 23 | 24 | Using the MCP client, you can retrieve the prompt like this using the STDIO transport: 25 | 26 | ```python 27 | import asyncio 28 | from mcp.client.session import ClientSession 29 | from mcp.client.stdio import StdioServerParameters, stdio_client 30 | 31 | 32 | async def main(): 33 | async with stdio_client( 34 | StdioServerParameters(command="uv", args=["run", "mcp-simple-prompt"]) 35 | ) as (read, write): 36 | async with ClientSession(read, write) as session: 37 | await session.initialize() 38 | 39 | # List available prompts 40 | prompts = await session.list_prompts() 41 | print(prompts) 42 | 43 | # Get the prompt with arguments 44 | prompt = await session.get_prompt( 45 | "simple", 46 | { 47 | "context": "User is a software developer", 48 | "topic": "Python async programming", 49 | }, 50 | ) 51 | print(prompt) 52 | 53 | 54 | asyncio.run(main()) 55 | ``` 56 | -------------------------------------------------------------------------------- /examples/servers/simple-prompt/mcp_simple_prompt/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /examples/servers/simple-prompt/mcp_simple_prompt/__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from .server import main 4 | 5 | sys.exit(main()) 6 | -------------------------------------------------------------------------------- /examples/servers/simple-prompt/mcp_simple_prompt/server.py: -------------------------------------------------------------------------------- 1 | import anyio 2 | import click 3 | import mcp.types as types 4 | from mcp.server.lowlevel import Server 5 | 6 | 7 | def create_messages( 8 | context: str | None = None, topic: str | None = None 9 | ) -> list[types.PromptMessage]: 10 | """Create the messages for the prompt.""" 11 | messages = [] 12 | 13 | # Add context if provided 14 | if context: 15 | messages.append( 16 | types.PromptMessage( 17 | role="user", 18 | content=types.TextContent( 19 | type="text", text=f"Here is some relevant context: {context}" 20 | ), 21 | ) 22 | ) 23 | 24 | # Add the main prompt 25 | prompt = "Please help me with " 26 | if topic: 27 | prompt += f"the following topic: {topic}" 28 | else: 29 | prompt += "whatever questions I may have." 30 | 31 | messages.append( 32 | types.PromptMessage( 33 | role="user", content=types.TextContent(type="text", text=prompt) 34 | ) 35 | ) 36 | 37 | return messages 38 | 39 | 40 | @click.command() 41 | @click.option("--port", default=8000, help="Port to listen on for SSE") 42 | @click.option( 43 | "--transport", 44 | type=click.Choice(["stdio", "sse"]), 45 | default="stdio", 46 | help="Transport type", 47 | ) 48 | def main(port: int, transport: str) -> int: 49 | app = Server("mcp-simple-prompt") 50 | 51 | @app.list_prompts() 52 | async def list_prompts() -> list[types.Prompt]: 53 | return [ 54 | types.Prompt( 55 | name="simple", 56 | description="A simple prompt that can take optional context and topic " 57 | "arguments", 58 | arguments=[ 59 | types.PromptArgument( 60 | name="context", 61 | description="Additional context to consider", 62 | required=False, 63 | ), 64 | types.PromptArgument( 65 | name="topic", 66 | description="Specific topic to focus on", 67 | required=False, 68 | ), 69 | ], 70 | ) 71 | ] 72 | 73 | @app.get_prompt() 74 | async def get_prompt( 75 | name: str, arguments: dict[str, str] | None = None 76 | ) -> types.GetPromptResult: 77 | if name != "simple": 78 | raise ValueError(f"Unknown prompt: {name}") 79 | 80 | if arguments is None: 81 | arguments = {} 82 | 83 | return types.GetPromptResult( 84 | messages=create_messages( 85 | context=arguments.get("context"), topic=arguments.get("topic") 86 | ), 87 | description="A simple prompt with optional context and topic arguments", 88 | ) 89 | 90 | if transport == "sse": 91 | from mcp.server.sse import SseServerTransport 92 | from starlette.applications import Starlette 93 | from starlette.routing import Mount, Route 94 | 95 | sse = SseServerTransport("/messages/") 96 | 97 | async def handle_sse(request): 98 | async with sse.connect_sse( 99 | request.scope, request.receive, request._send 100 | ) as streams: 101 | await app.run( 102 | streams[0], streams[1], app.create_initialization_options() 103 | ) 104 | 105 | starlette_app = Starlette( 106 | debug=True, 107 | routes=[ 108 | Route("/sse", endpoint=handle_sse), 109 | Mount("/messages/", app=sse.handle_post_message), 110 | ], 111 | ) 112 | 113 | import uvicorn 114 | 115 | uvicorn.run(starlette_app, host="0.0.0.0", port=port) 116 | else: 117 | from mcp.server.stdio import stdio_server 118 | 119 | async def arun(): 120 | async with stdio_server() as streams: 121 | await app.run( 122 | streams[0], streams[1], app.create_initialization_options() 123 | ) 124 | 125 | anyio.run(arun) 126 | 127 | return 0 128 | -------------------------------------------------------------------------------- /examples/servers/simple-prompt/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "mcp-simple-prompt" 3 | version = "0.1.0" 4 | description = "A simple MCP server exposing a customizable prompt" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | authors = [{ name = "Anthropic, PBC." }] 8 | maintainers = [ 9 | { name = "David Soria Parra", email = "davidsp@anthropic.com" }, 10 | { name = "Justin Spahr-Summers", email = "justin@anthropic.com" }, 11 | ] 12 | keywords = ["mcp", "llm", "automation", "web", "fetch"] 13 | license = { text = "MIT" } 14 | classifiers = [ 15 | "Development Status :: 4 - Beta", 16 | "Intended Audience :: Developers", 17 | "License :: OSI Approved :: MIT License", 18 | "Programming Language :: Python :: 3", 19 | "Programming Language :: Python :: 3.10", 20 | ] 21 | dependencies = ["anyio>=4.5", "click>=8.1.0", "httpx>=0.27", "mcp"] 22 | 23 | [project.scripts] 24 | mcp-simple-prompt = "mcp_simple_prompt.server:main" 25 | 26 | [build-system] 27 | requires = ["hatchling"] 28 | build-backend = "hatchling.build" 29 | 30 | [tool.hatch.build.targets.wheel] 31 | packages = ["mcp_simple_prompt"] 32 | 33 | [tool.pyright] 34 | include = ["mcp_simple_prompt"] 35 | venvPath = "." 36 | venv = ".venv" 37 | 38 | [tool.ruff.lint] 39 | select = ["E", "F", "I"] 40 | ignore = [] 41 | 42 | [tool.ruff] 43 | line-length = 88 44 | target-version = "py310" 45 | 46 | [tool.uv] 47 | dev-dependencies = ["pyright>=1.1.378", "pytest>=8.3.3", "ruff>=0.6.9"] 48 | -------------------------------------------------------------------------------- /examples/servers/simple-resource/.python-version: -------------------------------------------------------------------------------- 1 | 3.10 2 | -------------------------------------------------------------------------------- /examples/servers/simple-resource/README.md: -------------------------------------------------------------------------------- 1 | # MCP Simple Resource 2 | 3 | A simple MCP server that exposes sample text files as resources. 4 | 5 | ## Usage 6 | 7 | Start the server using either stdio (default) or SSE transport: 8 | 9 | ```bash 10 | # Using stdio transport (default) 11 | uv run mcp-simple-resource 12 | 13 | # Using SSE transport on custom port 14 | uv run mcp-simple-resource --transport sse --port 8000 15 | ``` 16 | 17 | The server exposes some basic text file resources that can be read by clients. 18 | 19 | ## Example 20 | 21 | Using the MCP client, you can retrieve resources like this using the STDIO transport: 22 | 23 | ```python 24 | import asyncio 25 | from mcp.types import AnyUrl 26 | from mcp.client.session import ClientSession 27 | from mcp.client.stdio import StdioServerParameters, stdio_client 28 | 29 | 30 | async def main(): 31 | async with stdio_client( 32 | StdioServerParameters(command="uv", args=["run", "mcp-simple-resource"]) 33 | ) as (read, write): 34 | async with ClientSession(read, write) as session: 35 | await session.initialize() 36 | 37 | # List available resources 38 | resources = await session.list_resources() 39 | print(resources) 40 | 41 | # Get a specific resource 42 | resource = await session.read_resource(AnyUrl("file:///greeting.txt")) 43 | print(resource) 44 | 45 | 46 | asyncio.run(main()) 47 | 48 | ``` 49 | -------------------------------------------------------------------------------- /examples/servers/simple-resource/mcp_simple_resource/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /examples/servers/simple-resource/mcp_simple_resource/__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from .server import main 4 | 5 | sys.exit(main()) 6 | -------------------------------------------------------------------------------- /examples/servers/simple-resource/mcp_simple_resource/server.py: -------------------------------------------------------------------------------- 1 | import anyio 2 | import click 3 | import mcp.types as types 4 | from mcp.server.lowlevel import Server 5 | from pydantic import FileUrl 6 | 7 | SAMPLE_RESOURCES = { 8 | "greeting": "Hello! This is a sample text resource.", 9 | "help": "This server provides a few sample text resources for testing.", 10 | "about": "This is the simple-resource MCP server implementation.", 11 | } 12 | 13 | 14 | @click.command() 15 | @click.option("--port", default=8000, help="Port to listen on for SSE") 16 | @click.option( 17 | "--transport", 18 | type=click.Choice(["stdio", "sse"]), 19 | default="stdio", 20 | help="Transport type", 21 | ) 22 | def main(port: int, transport: str) -> int: 23 | app = Server("mcp-simple-resource") 24 | 25 | @app.list_resources() 26 | async def list_resources() -> list[types.Resource]: 27 | return [ 28 | types.Resource( 29 | uri=FileUrl(f"file:///{name}.txt"), 30 | name=name, 31 | description=f"A sample text resource named {name}", 32 | mimeType="text/plain", 33 | ) 34 | for name in SAMPLE_RESOURCES.keys() 35 | ] 36 | 37 | @app.read_resource() 38 | async def read_resource(uri: FileUrl) -> str | bytes: 39 | name = uri.path.replace(".txt", "").lstrip("/") 40 | 41 | if name not in SAMPLE_RESOURCES: 42 | raise ValueError(f"Unknown resource: {uri}") 43 | 44 | return SAMPLE_RESOURCES[name] 45 | 46 | if transport == "sse": 47 | from mcp.server.sse import SseServerTransport 48 | from starlette.applications import Starlette 49 | from starlette.routing import Mount, Route 50 | 51 | sse = SseServerTransport("/messages/") 52 | 53 | async def handle_sse(request): 54 | async with sse.connect_sse( 55 | request.scope, request.receive, request._send 56 | ) as streams: 57 | await app.run( 58 | streams[0], streams[1], app.create_initialization_options() 59 | ) 60 | 61 | starlette_app = Starlette( 62 | debug=True, 63 | routes=[ 64 | Route("/sse", endpoint=handle_sse), 65 | Mount("/messages/", app=sse.handle_post_message), 66 | ], 67 | ) 68 | 69 | import uvicorn 70 | 71 | uvicorn.run(starlette_app, host="0.0.0.0", port=port) 72 | else: 73 | from mcp.server.stdio import stdio_server 74 | 75 | async def arun(): 76 | async with stdio_server() as streams: 77 | await app.run( 78 | streams[0], streams[1], app.create_initialization_options() 79 | ) 80 | 81 | anyio.run(arun) 82 | 83 | return 0 84 | -------------------------------------------------------------------------------- /examples/servers/simple-resource/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "mcp-simple-resource" 3 | version = "0.1.0" 4 | description = "A simple MCP server exposing sample text resources" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | authors = [{ name = "Anthropic, PBC." }] 8 | maintainers = [ 9 | { name = "David Soria Parra", email = "davidsp@anthropic.com" }, 10 | { name = "Justin Spahr-Summers", email = "justin@anthropic.com" }, 11 | ] 12 | keywords = ["mcp", "llm", "automation", "web", "fetch"] 13 | license = { text = "MIT" } 14 | classifiers = [ 15 | "Development Status :: 4 - Beta", 16 | "Intended Audience :: Developers", 17 | "License :: OSI Approved :: MIT License", 18 | "Programming Language :: Python :: 3", 19 | "Programming Language :: Python :: 3.10", 20 | ] 21 | dependencies = ["anyio>=4.5", "click>=8.1.0", "httpx>=0.27", "mcp"] 22 | 23 | [project.scripts] 24 | mcp-simple-resource = "mcp_simple_resource.server:main" 25 | 26 | [build-system] 27 | requires = ["hatchling"] 28 | build-backend = "hatchling.build" 29 | 30 | [tool.hatch.build.targets.wheel] 31 | packages = ["mcp_simple_resource"] 32 | 33 | [tool.pyright] 34 | include = ["mcp_simple_resource"] 35 | venvPath = "." 36 | venv = ".venv" 37 | 38 | [tool.ruff.lint] 39 | select = ["E", "F", "I"] 40 | ignore = [] 41 | 42 | [tool.ruff] 43 | line-length = 88 44 | target-version = "py310" 45 | 46 | [tool.uv] 47 | dev-dependencies = ["pyright>=1.1.378", "pytest>=8.3.3", "ruff>=0.6.9"] 48 | -------------------------------------------------------------------------------- /examples/servers/simple-tool/.python-version: -------------------------------------------------------------------------------- 1 | 3.10 2 | -------------------------------------------------------------------------------- /examples/servers/simple-tool/README.md: -------------------------------------------------------------------------------- 1 | 2 | A simple MCP server that exposes a website fetching tool. 3 | 4 | ## Usage 5 | 6 | Start the server using either stdio (default) or SSE transport: 7 | 8 | ```bash 9 | # Using stdio transport (default) 10 | uv run mcp-simple-tool 11 | 12 | # Using SSE transport on custom port 13 | uv run mcp-simple-tool --transport sse --port 8000 14 | ``` 15 | 16 | The server exposes a tool named "fetch" that accepts one required argument: 17 | 18 | - `url`: The URL of the website to fetch 19 | 20 | ## Example 21 | 22 | Using the MCP client, you can use the tool like this using the STDIO transport: 23 | 24 | ```python 25 | import asyncio 26 | from mcp.client.session import ClientSession 27 | from mcp.client.stdio import StdioServerParameters, stdio_client 28 | 29 | 30 | async def main(): 31 | async with stdio_client( 32 | StdioServerParameters(command="uv", args=["run", "mcp-simple-tool"]) 33 | ) as (read, write): 34 | async with ClientSession(read, write) as session: 35 | await session.initialize() 36 | 37 | # List available tools 38 | tools = await session.list_tools() 39 | print(tools) 40 | 41 | # Call the fetch tool 42 | result = await session.call_tool("fetch", {"url": "https://example.com"}) 43 | print(result) 44 | 45 | 46 | asyncio.run(main()) 47 | 48 | ``` 49 | -------------------------------------------------------------------------------- /examples/servers/simple-tool/mcp_simple_tool/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /examples/servers/simple-tool/mcp_simple_tool/__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from .server import main 4 | 5 | sys.exit(main()) 6 | -------------------------------------------------------------------------------- /examples/servers/simple-tool/mcp_simple_tool/server.py: -------------------------------------------------------------------------------- 1 | import anyio 2 | import click 3 | import httpx 4 | import mcp.types as types 5 | from mcp.server.lowlevel import Server 6 | 7 | 8 | async def fetch_website( 9 | url: str, 10 | ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]: 11 | headers = { 12 | "User-Agent": "MCP Test Server (github.com/modelcontextprotocol/python-sdk)" 13 | } 14 | async with httpx.AsyncClient(follow_redirects=True, headers=headers) as client: 15 | response = await client.get(url) 16 | response.raise_for_status() 17 | return [types.TextContent(type="text", text=response.text)] 18 | 19 | 20 | @click.command() 21 | @click.option("--port", default=8000, help="Port to listen on for SSE") 22 | @click.option( 23 | "--transport", 24 | type=click.Choice(["stdio", "sse"]), 25 | default="stdio", 26 | help="Transport type", 27 | ) 28 | def main(port: int, transport: str) -> int: 29 | app = Server("mcp-website-fetcher") 30 | 31 | @app.call_tool() 32 | async def fetch_tool( 33 | name: str, arguments: dict 34 | ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]: 35 | if name != "fetch": 36 | raise ValueError(f"Unknown tool: {name}") 37 | if "url" not in arguments: 38 | raise ValueError("Missing required argument 'url'") 39 | return await fetch_website(arguments["url"]) 40 | 41 | @app.list_tools() 42 | async def list_tools() -> list[types.Tool]: 43 | return [ 44 | types.Tool( 45 | name="fetch", 46 | description="Fetches a website and returns its content", 47 | inputSchema={ 48 | "type": "object", 49 | "required": ["url"], 50 | "properties": { 51 | "url": { 52 | "type": "string", 53 | "description": "URL to fetch", 54 | } 55 | }, 56 | }, 57 | ) 58 | ] 59 | 60 | if transport == "sse": 61 | from mcp.server.sse import SseServerTransport 62 | from starlette.applications import Starlette 63 | from starlette.routing import Mount, Route 64 | 65 | sse = SseServerTransport("/messages/") 66 | 67 | async def handle_sse(request): 68 | async with sse.connect_sse( 69 | request.scope, request.receive, request._send 70 | ) as streams: 71 | await app.run( 72 | streams[0], streams[1], app.create_initialization_options() 73 | ) 74 | 75 | starlette_app = Starlette( 76 | debug=True, 77 | routes=[ 78 | Route("/sse", endpoint=handle_sse), 79 | Mount("/messages/", app=sse.handle_post_message), 80 | ], 81 | ) 82 | 83 | import uvicorn 84 | 85 | uvicorn.run(starlette_app, host="0.0.0.0", port=port) 86 | else: 87 | from mcp.server.stdio import stdio_server 88 | 89 | async def arun(): 90 | async with stdio_server() as streams: 91 | await app.run( 92 | streams[0], streams[1], app.create_initialization_options() 93 | ) 94 | 95 | anyio.run(arun) 96 | 97 | return 0 98 | -------------------------------------------------------------------------------- /examples/servers/simple-tool/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "mcp-simple-tool" 3 | version = "0.1.0" 4 | description = "A simple MCP server exposing a website fetching tool" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | authors = [{ name = "Anthropic, PBC." }] 8 | maintainers = [ 9 | { name = "David Soria Parra", email = "davidsp@anthropic.com" }, 10 | { name = "Justin Spahr-Summers", email = "justin@anthropic.com" }, 11 | ] 12 | keywords = ["mcp", "llm", "automation", "web", "fetch"] 13 | license = { text = "MIT" } 14 | classifiers = [ 15 | "Development Status :: 4 - Beta", 16 | "Intended Audience :: Developers", 17 | "License :: OSI Approved :: MIT License", 18 | "Programming Language :: Python :: 3", 19 | "Programming Language :: Python :: 3.10", 20 | ] 21 | dependencies = ["anyio>=4.5", "click>=8.1.0", "httpx>=0.27", "mcp"] 22 | 23 | [project.scripts] 24 | mcp-simple-tool = "mcp_simple_tool.server:main" 25 | 26 | [build-system] 27 | requires = ["hatchling"] 28 | build-backend = "hatchling.build" 29 | 30 | [tool.hatch.build.targets.wheel] 31 | packages = ["mcp_simple_tool"] 32 | 33 | [tool.pyright] 34 | include = ["mcp_simple_tool"] 35 | venvPath = "." 36 | venv = ".venv" 37 | 38 | [tool.ruff.lint] 39 | select = ["E", "F", "I"] 40 | ignore = [] 41 | 42 | [tool.ruff] 43 | line-length = 88 44 | target-version = "py310" 45 | 46 | [tool.uv] 47 | dev-dependencies = ["pyright>=1.1.378", "pytest>=8.3.3", "ruff>=0.6.9"] 48 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: MCP Server 2 | site_description: MCP Server 3 | strict: true 4 | 5 | repo_name: modelcontextprotocol/python-sdk 6 | repo_url: https://github.com/modelcontextprotocol/python-sdk 7 | edit_uri: edit/main/docs/ 8 | site_url: https://modelcontextprotocol.github.io/python-sdk 9 | 10 | # TODO(Marcelo): Add Anthropic copyright? 11 | # copyright: © Model Context Protocol 2025 to present 12 | 13 | nav: 14 | - Home: index.md 15 | - API Reference: api.md 16 | 17 | theme: 18 | name: "material" 19 | palette: 20 | - media: "(prefers-color-scheme)" 21 | scheme: default 22 | primary: black 23 | accent: black 24 | toggle: 25 | icon: material/lightbulb 26 | name: "Switch to light mode" 27 | - media: "(prefers-color-scheme: light)" 28 | scheme: default 29 | primary: black 30 | accent: black 31 | toggle: 32 | icon: material/lightbulb-outline 33 | name: "Switch to dark mode" 34 | - media: "(prefers-color-scheme: dark)" 35 | scheme: slate 36 | primary: white 37 | accent: white 38 | toggle: 39 | icon: material/lightbulb-auto-outline 40 | name: "Switch to system preference" 41 | features: 42 | - search.suggest 43 | - search.highlight 44 | - content.tabs.link 45 | - content.code.annotate 46 | - content.code.copy 47 | - content.code.select 48 | - navigation.path 49 | - navigation.indexes 50 | - navigation.sections 51 | - navigation.tracking 52 | - toc.follow 53 | # logo: "img/logo-white.svg" 54 | # TODO(Marcelo): Add a favicon. 55 | # favicon: "favicon.ico" 56 | 57 | # https://www.mkdocs.org/user-guide/configuration/#validation 58 | validation: 59 | omitted_files: warn 60 | absolute_links: warn 61 | unrecognized_links: warn 62 | anchors: warn 63 | 64 | markdown_extensions: 65 | - tables 66 | - admonition 67 | - attr_list 68 | - md_in_html 69 | - pymdownx.details 70 | - pymdownx.caret 71 | - pymdownx.critic 72 | - pymdownx.mark 73 | - pymdownx.superfences 74 | - pymdownx.snippets 75 | - pymdownx.tilde 76 | - pymdownx.inlinehilite 77 | - pymdownx.highlight: 78 | pygments_lang_class: true 79 | - pymdownx.extra: 80 | pymdownx.superfences: 81 | custom_fences: 82 | - name: mermaid 83 | class: mermaid 84 | format: !!python/name:pymdownx.superfences.fence_code_format 85 | - pymdownx.emoji: 86 | emoji_index: !!python/name:material.extensions.emoji.twemoji 87 | emoji_generator: !!python/name:material.extensions.emoji.to_svg 88 | options: 89 | custom_icons: 90 | - docs/.overrides/.icons 91 | - pymdownx.tabbed: 92 | alternate_style: true 93 | - pymdownx.tasklist: 94 | custom_checkbox: true 95 | - sane_lists # this means you can start a list from any number 96 | 97 | watch: 98 | - src/mcp 99 | 100 | plugins: 101 | - search 102 | - social 103 | - glightbox 104 | - mkdocstrings: 105 | handlers: 106 | python: 107 | paths: [src/mcp] 108 | options: 109 | relative_crossrefs: true 110 | members_order: source 111 | separate_signature: true 112 | show_signature_annotations: true 113 | signature_crossrefs: true 114 | group_by_category: false 115 | # 3 because docs are in pages with an H2 just above them 116 | heading_level: 3 117 | import: 118 | - url: https://docs.python.org/3/objects.inv 119 | - url: https://docs.pydantic.dev/latest/objects.inv 120 | - url: https://typing-extensions.readthedocs.io/en/latest/objects.inv 121 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "mcp" 3 | dynamic = ["version"] 4 | description = "Model Context Protocol SDK" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | authors = [{ name = "Anthropic, PBC." }] 8 | maintainers = [ 9 | { name = "David Soria Parra", email = "davidsp@anthropic.com" }, 10 | { name = "Justin Spahr-Summers", email = "justin@anthropic.com" }, 11 | ] 12 | keywords = ["git", "mcp", "llm", "automation"] 13 | license = { text = "MIT" } 14 | classifiers = [ 15 | "Development Status :: 4 - Beta", 16 | "Intended Audience :: Developers", 17 | "License :: OSI Approved :: MIT License", 18 | "Programming Language :: Python :: 3", 19 | "Programming Language :: Python :: 3.10", 20 | "Programming Language :: Python :: 3.11", 21 | "Programming Language :: Python :: 3.12", 22 | "Programming Language :: Python :: 3.13", 23 | ] 24 | dependencies = [ 25 | "anyio>=4.5", 26 | "httpx>=0.27", 27 | "httpx-sse>=0.4", 28 | "pydantic>=2.7.2,<3.0.0", 29 | "starlette>=0.27", 30 | "sse-starlette>=1.6.1", 31 | "pydantic-settings>=2.5.2", 32 | "uvicorn>=0.23.1; sys_platform != 'emscripten'", 33 | ] 34 | 35 | [project.optional-dependencies] 36 | rich = ["rich>=13.9.4"] 37 | cli = ["typer>=0.12.4", "python-dotenv>=1.0.0"] 38 | ws = ["websockets>=15.0.1"] 39 | 40 | [project.scripts] 41 | mcp = "mcp.cli:app [cli]" 42 | 43 | [tool.uv] 44 | resolution = "lowest-direct" 45 | default-groups = ["dev", "docs"] 46 | 47 | [dependency-groups] 48 | dev = [ 49 | "pyright>=1.1.391", 50 | "pytest>=8.3.4", 51 | "ruff>=0.8.5", 52 | "trio>=0.26.2", 53 | "pytest-flakefinder>=1.1.0", 54 | "pytest-xdist>=3.6.1", 55 | "pytest-examples>=0.0.14", 56 | ] 57 | docs = [ 58 | "mkdocs>=1.6.1", 59 | "mkdocs-glightbox>=0.4.0", 60 | "mkdocs-material[imaging]>=9.5.45", 61 | "mkdocstrings-python>=1.12.2", 62 | ] 63 | 64 | 65 | [build-system] 66 | requires = ["hatchling", "uv-dynamic-versioning"] 67 | build-backend = "hatchling.build" 68 | 69 | [tool.hatch.version] 70 | source = "uv-dynamic-versioning" 71 | 72 | [tool.uv-dynamic-versioning] 73 | vcs = "git" 74 | style = "pep440" 75 | bump = true 76 | 77 | [project.urls] 78 | Homepage = "https://modelcontextprotocol.io" 79 | Repository = "https://github.com/modelcontextprotocol/python-sdk" 80 | Issues = "https://github.com/modelcontextprotocol/python-sdk/issues" 81 | 82 | [tool.hatch.build.targets.wheel] 83 | packages = ["src/mcp"] 84 | 85 | [tool.pyright] 86 | include = ["src/mcp", "tests"] 87 | venvPath = "." 88 | venv = ".venv" 89 | strict = ["src/mcp/**/*.py"] 90 | 91 | [tool.ruff.lint] 92 | select = ["C4", "E", "F", "I", "PERF", "UP"] 93 | ignore = ["PERF203"] 94 | 95 | [tool.ruff] 96 | line-length = 88 97 | target-version = "py310" 98 | 99 | [tool.ruff.lint.per-file-ignores] 100 | "__init__.py" = ["F401"] 101 | "tests/server/fastmcp/test_func_metadata.py" = ["E501"] 102 | 103 | [tool.uv.workspace] 104 | members = ["examples/servers/*"] 105 | 106 | [tool.uv.sources] 107 | mcp = { workspace = true } 108 | 109 | [tool.pytest.ini_options] 110 | xfail_strict = true 111 | filterwarnings = [ 112 | "error", 113 | # This should be fixed on Uvicorn's side. 114 | "ignore::DeprecationWarning:websockets", 115 | "ignore:websockets.server.WebSocketServerProtocol is deprecated:DeprecationWarning", 116 | "ignore:Returning str or bytes.*:DeprecationWarning:mcp.server.lowlevel" 117 | ] 118 | -------------------------------------------------------------------------------- /src/mcp/__init__.py: -------------------------------------------------------------------------------- 1 | from .client.session import ClientSession 2 | from .client.stdio import StdioServerParameters, stdio_client 3 | from .server.session import ServerSession 4 | from .server.stdio import stdio_server 5 | from .shared.exceptions import McpError 6 | from .types import ( 7 | CallToolRequest, 8 | ClientCapabilities, 9 | ClientNotification, 10 | ClientRequest, 11 | ClientResult, 12 | CompleteRequest, 13 | CreateMessageRequest, 14 | CreateMessageResult, 15 | ErrorData, 16 | GetPromptRequest, 17 | GetPromptResult, 18 | Implementation, 19 | IncludeContext, 20 | InitializedNotification, 21 | InitializeRequest, 22 | InitializeResult, 23 | JSONRPCError, 24 | JSONRPCRequest, 25 | JSONRPCResponse, 26 | ListPromptsRequest, 27 | ListPromptsResult, 28 | ListResourcesRequest, 29 | ListResourcesResult, 30 | ListToolsResult, 31 | LoggingLevel, 32 | LoggingMessageNotification, 33 | Notification, 34 | PingRequest, 35 | ProgressNotification, 36 | PromptsCapability, 37 | ReadResourceRequest, 38 | ReadResourceResult, 39 | Resource, 40 | ResourcesCapability, 41 | ResourceUpdatedNotification, 42 | RootsCapability, 43 | SamplingMessage, 44 | ServerCapabilities, 45 | ServerNotification, 46 | ServerRequest, 47 | ServerResult, 48 | SetLevelRequest, 49 | StopReason, 50 | SubscribeRequest, 51 | Tool, 52 | ToolsCapability, 53 | UnsubscribeRequest, 54 | ) 55 | from .types import ( 56 | Role as SamplingRole, 57 | ) 58 | 59 | __all__ = [ 60 | "CallToolRequest", 61 | "ClientCapabilities", 62 | "ClientNotification", 63 | "ClientRequest", 64 | "ClientResult", 65 | "ClientSession", 66 | "CreateMessageRequest", 67 | "CreateMessageResult", 68 | "ErrorData", 69 | "GetPromptRequest", 70 | "GetPromptResult", 71 | "Implementation", 72 | "IncludeContext", 73 | "InitializeRequest", 74 | "InitializeResult", 75 | "InitializedNotification", 76 | "JSONRPCError", 77 | "JSONRPCRequest", 78 | "ListPromptsRequest", 79 | "ListPromptsResult", 80 | "ListResourcesRequest", 81 | "ListResourcesResult", 82 | "ListToolsResult", 83 | "LoggingLevel", 84 | "LoggingMessageNotification", 85 | "McpError", 86 | "Notification", 87 | "PingRequest", 88 | "ProgressNotification", 89 | "PromptsCapability", 90 | "ReadResourceRequest", 91 | "ReadResourceResult", 92 | "ResourcesCapability", 93 | "ResourceUpdatedNotification", 94 | "Resource", 95 | "RootsCapability", 96 | "SamplingMessage", 97 | "SamplingRole", 98 | "ServerCapabilities", 99 | "ServerNotification", 100 | "ServerRequest", 101 | "ServerResult", 102 | "ServerSession", 103 | "SetLevelRequest", 104 | "StdioServerParameters", 105 | "StopReason", 106 | "SubscribeRequest", 107 | "Tool", 108 | "ToolsCapability", 109 | "UnsubscribeRequest", 110 | "stdio_client", 111 | "stdio_server", 112 | "CompleteRequest", 113 | "JSONRPCResponse", 114 | ] 115 | -------------------------------------------------------------------------------- /src/mcp/cli/__init__.py: -------------------------------------------------------------------------------- 1 | """FastMCP CLI package.""" 2 | 3 | from .cli import app 4 | 5 | if __name__ == "__main__": 6 | app() 7 | -------------------------------------------------------------------------------- /src/mcp/cli/claude.py: -------------------------------------------------------------------------------- 1 | """Claude app integration utilities.""" 2 | 3 | import json 4 | import os 5 | import sys 6 | from pathlib import Path 7 | from typing import Any 8 | 9 | from mcp.server.fastmcp.utilities.logging import get_logger 10 | 11 | logger = get_logger(__name__) 12 | 13 | MCP_PACKAGE = "mcp[cli]" 14 | 15 | 16 | def get_claude_config_path() -> Path | None: 17 | """Get the Claude config directory based on platform.""" 18 | if sys.platform == "win32": 19 | path = Path(Path.home(), "AppData", "Roaming", "Claude") 20 | elif sys.platform == "darwin": 21 | path = Path(Path.home(), "Library", "Application Support", "Claude") 22 | elif sys.platform.startswith("linux"): 23 | path = Path( 24 | os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"), "Claude" 25 | ) 26 | else: 27 | return None 28 | 29 | if path.exists(): 30 | return path 31 | return None 32 | 33 | 34 | def update_claude_config( 35 | file_spec: str, 36 | server_name: str, 37 | *, 38 | with_editable: Path | None = None, 39 | with_packages: list[str] | None = None, 40 | env_vars: dict[str, str] | None = None, 41 | ) -> bool: 42 | """Add or update a FastMCP server in Claude's configuration. 43 | 44 | Args: 45 | file_spec: Path to the server file, optionally with :object suffix 46 | server_name: Name for the server in Claude's config 47 | with_editable: Optional directory to install in editable mode 48 | with_packages: Optional list of additional packages to install 49 | env_vars: Optional dictionary of environment variables. These are merged with 50 | any existing variables, with new values taking precedence. 51 | 52 | Raises: 53 | RuntimeError: If Claude Desktop's config directory is not found, indicating 54 | Claude Desktop may not be installed or properly set up. 55 | """ 56 | config_dir = get_claude_config_path() 57 | if not config_dir: 58 | raise RuntimeError( 59 | "Claude Desktop config directory not found. Please ensure Claude Desktop" 60 | " is installed and has been run at least once to initialize its config." 61 | ) 62 | 63 | config_file = config_dir / "claude_desktop_config.json" 64 | if not config_file.exists(): 65 | try: 66 | config_file.write_text("{}") 67 | except Exception as e: 68 | logger.error( 69 | "Failed to create Claude config file", 70 | extra={ 71 | "error": str(e), 72 | "config_file": str(config_file), 73 | }, 74 | ) 75 | return False 76 | 77 | try: 78 | config = json.loads(config_file.read_text()) 79 | if "mcpServers" not in config: 80 | config["mcpServers"] = {} 81 | 82 | # Always preserve existing env vars and merge with new ones 83 | if ( 84 | server_name in config["mcpServers"] 85 | and "env" in config["mcpServers"][server_name] 86 | ): 87 | existing_env = config["mcpServers"][server_name]["env"] 88 | if env_vars: 89 | # New vars take precedence over existing ones 90 | env_vars = {**existing_env, **env_vars} 91 | else: 92 | env_vars = existing_env 93 | 94 | # Build uv run command 95 | args = ["run"] 96 | 97 | # Collect all packages in a set to deduplicate 98 | packages = {MCP_PACKAGE} 99 | if with_packages: 100 | packages.update(pkg for pkg in with_packages if pkg) 101 | 102 | # Add all packages with --with 103 | for pkg in sorted(packages): 104 | args.extend(["--with", pkg]) 105 | 106 | if with_editable: 107 | args.extend(["--with-editable", str(with_editable)]) 108 | 109 | # Convert file path to absolute before adding to command 110 | # Split off any :object suffix first 111 | if ":" in file_spec: 112 | file_path, server_object = file_spec.rsplit(":", 1) 113 | file_spec = f"{Path(file_path).resolve()}:{server_object}" 114 | else: 115 | file_spec = str(Path(file_spec).resolve()) 116 | 117 | # Add fastmcp run command 118 | args.extend(["mcp", "run", file_spec]) 119 | 120 | server_config: dict[str, Any] = {"command": "uv", "args": args} 121 | 122 | # Add environment variables if specified 123 | if env_vars: 124 | server_config["env"] = env_vars 125 | 126 | config["mcpServers"][server_name] = server_config 127 | 128 | config_file.write_text(json.dumps(config, indent=2)) 129 | logger.info( 130 | f"Added server '{server_name}' to Claude config", 131 | extra={"config_file": str(config_file)}, 132 | ) 133 | return True 134 | except Exception as e: 135 | logger.error( 136 | "Failed to update Claude config", 137 | extra={ 138 | "error": str(e), 139 | "config_file": str(config_file), 140 | }, 141 | ) 142 | return False 143 | -------------------------------------------------------------------------------- /src/mcp/client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/b4c7db6a50a5c88bae1db5c1f7fba44d16eebc6e/src/mcp/client/__init__.py -------------------------------------------------------------------------------- /src/mcp/client/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | import sys 4 | from functools import partial 5 | from urllib.parse import urlparse 6 | 7 | import anyio 8 | from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream 9 | 10 | import mcp.types as types 11 | from mcp.client.session import ClientSession 12 | from mcp.client.sse import sse_client 13 | from mcp.client.stdio import StdioServerParameters, stdio_client 14 | from mcp.shared.session import RequestResponder 15 | from mcp.types import JSONRPCMessage 16 | 17 | if not sys.warnoptions: 18 | import warnings 19 | 20 | warnings.simplefilter("ignore") 21 | 22 | logging.basicConfig(level=logging.INFO) 23 | logger = logging.getLogger("client") 24 | 25 | 26 | async def message_handler( 27 | message: RequestResponder[types.ServerRequest, types.ClientResult] 28 | | types.ServerNotification 29 | | Exception, 30 | ) -> None: 31 | if isinstance(message, Exception): 32 | logger.error("Error: %s", message) 33 | return 34 | 35 | logger.info("Received message from server: %s", message) 36 | 37 | 38 | async def run_session( 39 | read_stream: MemoryObjectReceiveStream[JSONRPCMessage | Exception], 40 | write_stream: MemoryObjectSendStream[JSONRPCMessage], 41 | client_info: types.Implementation | None = None, 42 | ): 43 | async with ClientSession( 44 | read_stream, 45 | write_stream, 46 | message_handler=message_handler, 47 | client_info=client_info, 48 | ) as session: 49 | logger.info("Initializing session") 50 | await session.initialize() 51 | logger.info("Initialized") 52 | 53 | 54 | async def main(command_or_url: str, args: list[str], env: list[tuple[str, str]]): 55 | env_dict = dict(env) 56 | 57 | if urlparse(command_or_url).scheme in ("http", "https"): 58 | # Use SSE client for HTTP(S) URLs 59 | async with sse_client(command_or_url) as streams: 60 | await run_session(*streams) 61 | else: 62 | # Use stdio client for commands 63 | server_parameters = StdioServerParameters( 64 | command=command_or_url, args=args, env=env_dict 65 | ) 66 | async with stdio_client(server_parameters) as streams: 67 | await run_session(*streams) 68 | 69 | 70 | def cli(): 71 | parser = argparse.ArgumentParser() 72 | parser.add_argument("command_or_url", help="Command or URL to connect to") 73 | parser.add_argument("args", nargs="*", help="Additional arguments") 74 | parser.add_argument( 75 | "-e", 76 | "--env", 77 | nargs=2, 78 | action="append", 79 | metavar=("KEY", "VALUE"), 80 | help="Environment variables to set. Can be used multiple times.", 81 | default=[], 82 | ) 83 | 84 | args = parser.parse_args() 85 | anyio.run(partial(main, args.command_or_url, args.args, args.env), backend="trio") 86 | 87 | 88 | if __name__ == "__main__": 89 | cli() 90 | -------------------------------------------------------------------------------- /src/mcp/client/stdio/win32.py: -------------------------------------------------------------------------------- 1 | """ 2 | Windows-specific functionality for stdio client operations. 3 | """ 4 | 5 | import shutil 6 | import subprocess 7 | import sys 8 | from pathlib import Path 9 | from typing import TextIO 10 | 11 | import anyio 12 | from anyio.abc import Process 13 | 14 | 15 | def get_windows_executable_command(command: str) -> str: 16 | """ 17 | Get the correct executable command normalized for Windows. 18 | 19 | On Windows, commands might exist with specific extensions (.exe, .cmd, etc.) 20 | that need to be located for proper execution. 21 | 22 | Args: 23 | command: Base command (e.g., 'uvx', 'npx') 24 | 25 | Returns: 26 | str: Windows-appropriate command path 27 | """ 28 | try: 29 | # First check if command exists in PATH as-is 30 | if command_path := shutil.which(command): 31 | return command_path 32 | 33 | # Check for Windows-specific extensions 34 | for ext in [".cmd", ".bat", ".exe", ".ps1"]: 35 | ext_version = f"{command}{ext}" 36 | if ext_path := shutil.which(ext_version): 37 | return ext_path 38 | 39 | # For regular commands or if we couldn't find special versions 40 | return command 41 | except OSError: 42 | # Handle file system errors during path resolution 43 | # (permissions, broken symlinks, etc.) 44 | return command 45 | 46 | 47 | async def create_windows_process( 48 | command: str, 49 | args: list[str], 50 | env: dict[str, str] | None = None, 51 | errlog: TextIO = sys.stderr, 52 | cwd: Path | str | None = None, 53 | ): 54 | """ 55 | Creates a subprocess in a Windows-compatible way. 56 | 57 | Windows processes need special handling for console windows and 58 | process creation flags. 59 | 60 | Args: 61 | command: The command to execute 62 | args: Command line arguments 63 | env: Environment variables 64 | errlog: Where to send stderr output 65 | cwd: Working directory for the process 66 | 67 | Returns: 68 | A process handle 69 | """ 70 | try: 71 | # Try with Windows-specific flags to hide console window 72 | process = await anyio.open_process( 73 | [command, *args], 74 | env=env, 75 | # Ensure we don't create console windows for each process 76 | creationflags=subprocess.CREATE_NO_WINDOW # type: ignore 77 | if hasattr(subprocess, "CREATE_NO_WINDOW") 78 | else 0, 79 | stderr=errlog, 80 | cwd=cwd, 81 | ) 82 | return process 83 | except Exception: 84 | # Don't raise, let's try to create the process without creation flags 85 | process = await anyio.open_process( 86 | [command, *args], env=env, stderr=errlog, cwd=cwd 87 | ) 88 | return process 89 | 90 | 91 | async def terminate_windows_process(process: Process): 92 | """ 93 | Terminate a Windows process. 94 | 95 | Note: On Windows, terminating a process with process.terminate() doesn't 96 | always guarantee immediate process termination. 97 | So we give it 2s to exit, or we call process.kill() 98 | which sends a SIGKILL equivalent signal. 99 | 100 | Args: 101 | process: The process to terminate 102 | """ 103 | try: 104 | process.terminate() 105 | with anyio.fail_after(2.0): 106 | await process.wait() 107 | except TimeoutError: 108 | # Force kill if it doesn't terminate 109 | process.kill() 110 | -------------------------------------------------------------------------------- /src/mcp/client/websocket.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | from collections.abc import AsyncGenerator 4 | from contextlib import asynccontextmanager 5 | 6 | import anyio 7 | from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream 8 | from pydantic import ValidationError 9 | from websockets.asyncio.client import connect as ws_connect 10 | from websockets.typing import Subprotocol 11 | 12 | import mcp.types as types 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | @asynccontextmanager 18 | async def websocket_client( 19 | url: str, 20 | ) -> AsyncGenerator[ 21 | tuple[ 22 | MemoryObjectReceiveStream[types.JSONRPCMessage | Exception], 23 | MemoryObjectSendStream[types.JSONRPCMessage], 24 | ], 25 | None, 26 | ]: 27 | """ 28 | WebSocket client transport for MCP, symmetrical to the server version. 29 | 30 | Connects to 'url' using the 'mcp' subprotocol, then yields: 31 | (read_stream, write_stream) 32 | 33 | - read_stream: As you read from this stream, you'll receive either valid 34 | JSONRPCMessage objects or Exception objects (when validation fails). 35 | - write_stream: Write JSONRPCMessage objects to this stream to send them 36 | over the WebSocket to the server. 37 | """ 38 | 39 | # Create two in-memory streams: 40 | # - One for incoming messages (read_stream, written by ws_reader) 41 | # - One for outgoing messages (write_stream, read by ws_writer) 42 | read_stream: MemoryObjectReceiveStream[types.JSONRPCMessage | Exception] 43 | read_stream_writer: MemoryObjectSendStream[types.JSONRPCMessage | Exception] 44 | write_stream: MemoryObjectSendStream[types.JSONRPCMessage] 45 | write_stream_reader: MemoryObjectReceiveStream[types.JSONRPCMessage] 46 | 47 | read_stream_writer, read_stream = anyio.create_memory_object_stream(0) 48 | write_stream, write_stream_reader = anyio.create_memory_object_stream(0) 49 | 50 | # Connect using websockets, requesting the "mcp" subprotocol 51 | async with ws_connect(url, subprotocols=[Subprotocol("mcp")]) as ws: 52 | 53 | async def ws_reader(): 54 | """ 55 | Reads text messages from the WebSocket, parses them as JSON-RPC messages, 56 | and sends them into read_stream_writer. 57 | """ 58 | async with read_stream_writer: 59 | async for raw_text in ws: 60 | try: 61 | message = types.JSONRPCMessage.model_validate_json(raw_text) 62 | await read_stream_writer.send(message) 63 | except ValidationError as exc: 64 | # If JSON parse or model validation fails, send the exception 65 | await read_stream_writer.send(exc) 66 | 67 | async def ws_writer(): 68 | """ 69 | Reads JSON-RPC messages from write_stream_reader and 70 | sends them to the server. 71 | """ 72 | async with write_stream_reader: 73 | async for message in write_stream_reader: 74 | # Convert to a dict, then to JSON 75 | msg_dict = message.model_dump( 76 | by_alias=True, mode="json", exclude_none=True 77 | ) 78 | await ws.send(json.dumps(msg_dict)) 79 | 80 | async with anyio.create_task_group() as tg: 81 | # Start reader and writer tasks 82 | tg.start_soon(ws_reader) 83 | tg.start_soon(ws_writer) 84 | 85 | # Yield the receive/send streams 86 | yield (read_stream, write_stream) 87 | 88 | # Once the caller's 'async with' block exits, we shut down 89 | tg.cancel_scope.cancel() 90 | -------------------------------------------------------------------------------- /src/mcp/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/b4c7db6a50a5c88bae1db5c1f7fba44d16eebc6e/src/mcp/py.typed -------------------------------------------------------------------------------- /src/mcp/server/__init__.py: -------------------------------------------------------------------------------- 1 | from .fastmcp import FastMCP 2 | from .lowlevel import NotificationOptions, Server 3 | from .models import InitializationOptions 4 | 5 | __all__ = ["Server", "FastMCP", "NotificationOptions", "InitializationOptions"] 6 | -------------------------------------------------------------------------------- /src/mcp/server/__main__.py: -------------------------------------------------------------------------------- 1 | import importlib.metadata 2 | import logging 3 | import sys 4 | 5 | import anyio 6 | 7 | from mcp.server.models import InitializationOptions 8 | from mcp.server.session import ServerSession 9 | from mcp.server.stdio import stdio_server 10 | from mcp.types import ServerCapabilities 11 | 12 | if not sys.warnoptions: 13 | import warnings 14 | 15 | warnings.simplefilter("ignore") 16 | 17 | logging.basicConfig(level=logging.INFO) 18 | logger = logging.getLogger("server") 19 | 20 | 21 | async def receive_loop(session: ServerSession): 22 | logger.info("Starting receive loop") 23 | async for message in session.incoming_messages: 24 | if isinstance(message, Exception): 25 | logger.error("Error: %s", message) 26 | continue 27 | 28 | logger.info("Received message from client: %s", message) 29 | 30 | 31 | async def main(): 32 | version = importlib.metadata.version("mcp") 33 | async with stdio_server() as (read_stream, write_stream): 34 | async with ( 35 | ServerSession( 36 | read_stream, 37 | write_stream, 38 | InitializationOptions( 39 | server_name="mcp", 40 | server_version=version, 41 | capabilities=ServerCapabilities(), 42 | ), 43 | ) as session, 44 | write_stream, 45 | ): 46 | await receive_loop(session) 47 | 48 | 49 | if __name__ == "__main__": 50 | anyio.run(main, backend="trio") 51 | -------------------------------------------------------------------------------- /src/mcp/server/fastmcp/__init__.py: -------------------------------------------------------------------------------- 1 | """FastMCP - A more ergonomic interface for MCP servers.""" 2 | 3 | from importlib.metadata import version 4 | 5 | from .server import Context, FastMCP 6 | from .utilities.types import Image 7 | 8 | __version__ = version("mcp") 9 | __all__ = ["FastMCP", "Context", "Image"] 10 | -------------------------------------------------------------------------------- /src/mcp/server/fastmcp/exceptions.py: -------------------------------------------------------------------------------- 1 | """Custom exceptions for FastMCP.""" 2 | 3 | 4 | class FastMCPError(Exception): 5 | """Base error for FastMCP.""" 6 | 7 | 8 | class ValidationError(FastMCPError): 9 | """Error in validating parameters or return values.""" 10 | 11 | 12 | class ResourceError(FastMCPError): 13 | """Error in resource operations.""" 14 | 15 | 16 | class ToolError(FastMCPError): 17 | """Error in tool operations.""" 18 | 19 | 20 | class InvalidSignature(Exception): 21 | """Invalid signature for use with FastMCP.""" 22 | -------------------------------------------------------------------------------- /src/mcp/server/fastmcp/prompts/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import Prompt 2 | from .manager import PromptManager 3 | 4 | __all__ = ["Prompt", "PromptManager"] 5 | -------------------------------------------------------------------------------- /src/mcp/server/fastmcp/prompts/manager.py: -------------------------------------------------------------------------------- 1 | """Prompt management functionality.""" 2 | 3 | from typing import Any 4 | 5 | from mcp.server.fastmcp.prompts.base import Message, Prompt 6 | from mcp.server.fastmcp.utilities.logging import get_logger 7 | 8 | logger = get_logger(__name__) 9 | 10 | 11 | class PromptManager: 12 | """Manages FastMCP prompts.""" 13 | 14 | def __init__(self, warn_on_duplicate_prompts: bool = True): 15 | self._prompts: dict[str, Prompt] = {} 16 | self.warn_on_duplicate_prompts = warn_on_duplicate_prompts 17 | 18 | def get_prompt(self, name: str) -> Prompt | None: 19 | """Get prompt by name.""" 20 | return self._prompts.get(name) 21 | 22 | def list_prompts(self) -> list[Prompt]: 23 | """List all registered prompts.""" 24 | return list(self._prompts.values()) 25 | 26 | def add_prompt( 27 | self, 28 | prompt: Prompt, 29 | ) -> Prompt: 30 | """Add a prompt to the manager.""" 31 | 32 | # Check for duplicates 33 | existing = self._prompts.get(prompt.name) 34 | if existing: 35 | if self.warn_on_duplicate_prompts: 36 | logger.warning(f"Prompt already exists: {prompt.name}") 37 | return existing 38 | 39 | self._prompts[prompt.name] = prompt 40 | return prompt 41 | 42 | async def render_prompt( 43 | self, name: str, arguments: dict[str, Any] | None = None 44 | ) -> list[Message]: 45 | """Render a prompt by name with arguments.""" 46 | prompt = self.get_prompt(name) 47 | if not prompt: 48 | raise ValueError(f"Unknown prompt: {name}") 49 | 50 | return await prompt.render(arguments) 51 | -------------------------------------------------------------------------------- /src/mcp/server/fastmcp/prompts/prompt_manager.py: -------------------------------------------------------------------------------- 1 | """Prompt management functionality.""" 2 | 3 | from mcp.server.fastmcp.prompts.base import Prompt 4 | from mcp.server.fastmcp.utilities.logging import get_logger 5 | 6 | logger = get_logger(__name__) 7 | 8 | 9 | class PromptManager: 10 | """Manages FastMCP prompts.""" 11 | 12 | def __init__(self, warn_on_duplicate_prompts: bool = True): 13 | self._prompts: dict[str, Prompt] = {} 14 | self.warn_on_duplicate_prompts = warn_on_duplicate_prompts 15 | 16 | def add_prompt(self, prompt: Prompt) -> Prompt: 17 | """Add a prompt to the manager.""" 18 | logger.debug(f"Adding prompt: {prompt.name}") 19 | existing = self._prompts.get(prompt.name) 20 | if existing: 21 | if self.warn_on_duplicate_prompts: 22 | logger.warning(f"Prompt already exists: {prompt.name}") 23 | return existing 24 | self._prompts[prompt.name] = prompt 25 | return prompt 26 | 27 | def get_prompt(self, name: str) -> Prompt | None: 28 | """Get prompt by name.""" 29 | return self._prompts.get(name) 30 | 31 | def list_prompts(self) -> list[Prompt]: 32 | """List all registered prompts.""" 33 | return list(self._prompts.values()) 34 | -------------------------------------------------------------------------------- /src/mcp/server/fastmcp/resources/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import Resource 2 | from .resource_manager import ResourceManager 3 | from .templates import ResourceTemplate 4 | from .types import ( 5 | BinaryResource, 6 | DirectoryResource, 7 | FileResource, 8 | FunctionResource, 9 | HttpResource, 10 | TextResource, 11 | ) 12 | 13 | __all__ = [ 14 | "Resource", 15 | "TextResource", 16 | "BinaryResource", 17 | "FunctionResource", 18 | "FileResource", 19 | "HttpResource", 20 | "DirectoryResource", 21 | "ResourceTemplate", 22 | "ResourceManager", 23 | ] 24 | -------------------------------------------------------------------------------- /src/mcp/server/fastmcp/resources/base.py: -------------------------------------------------------------------------------- 1 | """Base classes and interfaces for FastMCP resources.""" 2 | 3 | import abc 4 | from typing import Annotated 5 | 6 | from pydantic import ( 7 | AnyUrl, 8 | BaseModel, 9 | ConfigDict, 10 | Field, 11 | UrlConstraints, 12 | ValidationInfo, 13 | field_validator, 14 | ) 15 | 16 | 17 | class Resource(BaseModel, abc.ABC): 18 | """Base class for all resources.""" 19 | 20 | model_config = ConfigDict(validate_default=True) 21 | 22 | uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] = Field( 23 | default=..., description="URI of the resource" 24 | ) 25 | name: str | None = Field(description="Name of the resource", default=None) 26 | description: str | None = Field( 27 | description="Description of the resource", default=None 28 | ) 29 | mime_type: str = Field( 30 | default="text/plain", 31 | description="MIME type of the resource content", 32 | pattern=r"^[a-zA-Z0-9]+/[a-zA-Z0-9\-+.]+$", 33 | ) 34 | 35 | @field_validator("name", mode="before") 36 | @classmethod 37 | def set_default_name(cls, name: str | None, info: ValidationInfo) -> str: 38 | """Set default name from URI if not provided.""" 39 | if name: 40 | return name 41 | if uri := info.data.get("uri"): 42 | return str(uri) 43 | raise ValueError("Either name or uri must be provided") 44 | 45 | @abc.abstractmethod 46 | async def read(self) -> str | bytes: 47 | """Read the resource content.""" 48 | pass 49 | -------------------------------------------------------------------------------- /src/mcp/server/fastmcp/resources/resource_manager.py: -------------------------------------------------------------------------------- 1 | """Resource manager functionality.""" 2 | 3 | from collections.abc import Callable 4 | from typing import Any 5 | 6 | from pydantic import AnyUrl 7 | 8 | from mcp.server.fastmcp.resources.base import Resource 9 | from mcp.server.fastmcp.resources.templates import ResourceTemplate 10 | from mcp.server.fastmcp.utilities.logging import get_logger 11 | 12 | logger = get_logger(__name__) 13 | 14 | 15 | class ResourceManager: 16 | """Manages FastMCP resources.""" 17 | 18 | def __init__(self, warn_on_duplicate_resources: bool = True): 19 | self._resources: dict[str, Resource] = {} 20 | self._templates: dict[str, ResourceTemplate] = {} 21 | self.warn_on_duplicate_resources = warn_on_duplicate_resources 22 | 23 | def add_resource(self, resource: Resource) -> Resource: 24 | """Add a resource to the manager. 25 | 26 | Args: 27 | resource: A Resource instance to add 28 | 29 | Returns: 30 | The added resource. If a resource with the same URI already exists, 31 | returns the existing resource. 32 | """ 33 | logger.debug( 34 | "Adding resource", 35 | extra={ 36 | "uri": resource.uri, 37 | "type": type(resource).__name__, 38 | "resource_name": resource.name, 39 | }, 40 | ) 41 | existing = self._resources.get(str(resource.uri)) 42 | if existing: 43 | if self.warn_on_duplicate_resources: 44 | logger.warning(f"Resource already exists: {resource.uri}") 45 | return existing 46 | self._resources[str(resource.uri)] = resource 47 | return resource 48 | 49 | def add_template( 50 | self, 51 | fn: Callable[..., Any], 52 | uri_template: str, 53 | name: str | None = None, 54 | description: str | None = None, 55 | mime_type: str | None = None, 56 | ) -> ResourceTemplate: 57 | """Add a template from a function.""" 58 | template = ResourceTemplate.from_function( 59 | fn, 60 | uri_template=uri_template, 61 | name=name, 62 | description=description, 63 | mime_type=mime_type, 64 | ) 65 | self._templates[template.uri_template] = template 66 | return template 67 | 68 | async def get_resource(self, uri: AnyUrl | str) -> Resource | None: 69 | """Get resource by URI, checking concrete resources first, then templates.""" 70 | uri_str = str(uri) 71 | logger.debug("Getting resource", extra={"uri": uri_str}) 72 | 73 | # First check concrete resources 74 | if resource := self._resources.get(uri_str): 75 | return resource 76 | 77 | # Then check templates 78 | for template in self._templates.values(): 79 | if params := template.matches(uri_str): 80 | try: 81 | return await template.create_resource(uri_str, params) 82 | except Exception as e: 83 | raise ValueError(f"Error creating resource from template: {e}") 84 | 85 | raise ValueError(f"Unknown resource: {uri}") 86 | 87 | def list_resources(self) -> list[Resource]: 88 | """List all registered resources.""" 89 | logger.debug("Listing resources", extra={"count": len(self._resources)}) 90 | return list(self._resources.values()) 91 | 92 | def list_templates(self) -> list[ResourceTemplate]: 93 | """List all registered templates.""" 94 | logger.debug("Listing templates", extra={"count": len(self._templates)}) 95 | return list(self._templates.values()) 96 | -------------------------------------------------------------------------------- /src/mcp/server/fastmcp/resources/templates.py: -------------------------------------------------------------------------------- 1 | """Resource template functionality.""" 2 | 3 | from __future__ import annotations 4 | 5 | import inspect 6 | import re 7 | from collections.abc import Callable 8 | from typing import Any 9 | 10 | from pydantic import BaseModel, Field, TypeAdapter, validate_call 11 | 12 | from mcp.server.fastmcp.resources.types import FunctionResource, Resource 13 | 14 | 15 | class ResourceTemplate(BaseModel): 16 | """A template for dynamically creating resources.""" 17 | 18 | uri_template: str = Field( 19 | description="URI template with parameters (e.g. weather://{city}/current)" 20 | ) 21 | name: str = Field(description="Name of the resource") 22 | description: str | None = Field(description="Description of what the resource does") 23 | mime_type: str = Field( 24 | default="text/plain", description="MIME type of the resource content" 25 | ) 26 | fn: Callable[..., Any] = Field(exclude=True) 27 | parameters: dict[str, Any] = Field( 28 | description="JSON schema for function parameters" 29 | ) 30 | 31 | @classmethod 32 | def from_function( 33 | cls, 34 | fn: Callable[..., Any], 35 | uri_template: str, 36 | name: str | None = None, 37 | description: str | None = None, 38 | mime_type: str | None = None, 39 | ) -> ResourceTemplate: 40 | """Create a template from a function.""" 41 | func_name = name or fn.__name__ 42 | if func_name == "": 43 | raise ValueError("You must provide a name for lambda functions") 44 | 45 | # Get schema from TypeAdapter - will fail if function isn't properly typed 46 | parameters = TypeAdapter(fn).json_schema() 47 | 48 | # ensure the arguments are properly cast 49 | fn = validate_call(fn) 50 | 51 | return cls( 52 | uri_template=uri_template, 53 | name=func_name, 54 | description=description or fn.__doc__ or "", 55 | mime_type=mime_type or "text/plain", 56 | fn=fn, 57 | parameters=parameters, 58 | ) 59 | 60 | def matches(self, uri: str) -> dict[str, Any] | None: 61 | """Check if URI matches template and extract parameters.""" 62 | # Convert template to regex pattern 63 | pattern = self.uri_template.replace("{", "(?P<").replace("}", ">[^/]+)") 64 | match = re.match(f"^{pattern}$", uri) 65 | if match: 66 | return match.groupdict() 67 | return None 68 | 69 | async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource: 70 | """Create a resource from the template with the given parameters.""" 71 | try: 72 | # Call function and check if result is a coroutine 73 | result = self.fn(**params) 74 | if inspect.iscoroutine(result): 75 | result = await result 76 | 77 | return FunctionResource( 78 | uri=uri, # type: ignore 79 | name=self.name, 80 | description=self.description, 81 | mime_type=self.mime_type, 82 | fn=lambda: result, # Capture result in closure 83 | ) 84 | except Exception as e: 85 | raise ValueError(f"Error creating resource from template: {e}") 86 | -------------------------------------------------------------------------------- /src/mcp/server/fastmcp/tools/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import Tool 2 | from .tool_manager import ToolManager 3 | 4 | __all__ = ["Tool", "ToolManager"] 5 | -------------------------------------------------------------------------------- /src/mcp/server/fastmcp/tools/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations as _annotations 2 | 3 | import inspect 4 | from collections.abc import Callable 5 | from typing import TYPE_CHECKING, Any, get_origin 6 | 7 | from pydantic import BaseModel, Field 8 | 9 | from mcp.server.fastmcp.exceptions import ToolError 10 | from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata, func_metadata 11 | 12 | if TYPE_CHECKING: 13 | from mcp.server.fastmcp.server import Context 14 | from mcp.server.session import ServerSessionT 15 | from mcp.shared.context import LifespanContextT 16 | 17 | 18 | class Tool(BaseModel): 19 | """Internal tool registration info.""" 20 | 21 | fn: Callable[..., Any] = Field(exclude=True) 22 | name: str = Field(description="Name of the tool") 23 | description: str = Field(description="Description of what the tool does") 24 | parameters: dict[str, Any] = Field(description="JSON schema for tool parameters") 25 | fn_metadata: FuncMetadata = Field( 26 | description="Metadata about the function including a pydantic model for tool" 27 | " arguments" 28 | ) 29 | is_async: bool = Field(description="Whether the tool is async") 30 | context_kwarg: str | None = Field( 31 | None, description="Name of the kwarg that should receive context" 32 | ) 33 | 34 | @classmethod 35 | def from_function( 36 | cls, 37 | fn: Callable[..., Any], 38 | name: str | None = None, 39 | description: str | None = None, 40 | context_kwarg: str | None = None, 41 | ) -> Tool: 42 | """Create a Tool from a function.""" 43 | from mcp.server.fastmcp import Context 44 | 45 | func_name = name or fn.__name__ 46 | 47 | if func_name == "": 48 | raise ValueError("You must provide a name for lambda functions") 49 | 50 | func_doc = description or fn.__doc__ or "" 51 | is_async = inspect.iscoroutinefunction(fn) 52 | 53 | if context_kwarg is None: 54 | sig = inspect.signature(fn) 55 | for param_name, param in sig.parameters.items(): 56 | if get_origin(param.annotation) is not None: 57 | continue 58 | if issubclass(param.annotation, Context): 59 | context_kwarg = param_name 60 | break 61 | 62 | func_arg_metadata = func_metadata( 63 | fn, 64 | skip_names=[context_kwarg] if context_kwarg is not None else [], 65 | ) 66 | parameters = func_arg_metadata.arg_model.model_json_schema() 67 | 68 | return cls( 69 | fn=fn, 70 | name=func_name, 71 | description=func_doc, 72 | parameters=parameters, 73 | fn_metadata=func_arg_metadata, 74 | is_async=is_async, 75 | context_kwarg=context_kwarg, 76 | ) 77 | 78 | async def run( 79 | self, 80 | arguments: dict[str, Any], 81 | context: Context[ServerSessionT, LifespanContextT] | None = None, 82 | ) -> Any: 83 | """Run the tool with arguments.""" 84 | try: 85 | return await self.fn_metadata.call_fn_with_arg_validation( 86 | self.fn, 87 | self.is_async, 88 | arguments, 89 | {self.context_kwarg: context} 90 | if self.context_kwarg is not None 91 | else None, 92 | ) 93 | except Exception as e: 94 | raise ToolError(f"Error executing tool {self.name}: {e}") from e 95 | -------------------------------------------------------------------------------- /src/mcp/server/fastmcp/tools/tool_manager.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations as _annotations 2 | 3 | from collections.abc import Callable 4 | from typing import TYPE_CHECKING, Any 5 | 6 | from mcp.server.fastmcp.exceptions import ToolError 7 | from mcp.server.fastmcp.tools.base import Tool 8 | from mcp.server.fastmcp.utilities.logging import get_logger 9 | from mcp.shared.context import LifespanContextT 10 | 11 | if TYPE_CHECKING: 12 | from mcp.server.fastmcp.server import Context 13 | from mcp.server.session import ServerSessionT 14 | 15 | logger = get_logger(__name__) 16 | 17 | 18 | class ToolManager: 19 | """Manages FastMCP tools.""" 20 | 21 | def __init__(self, warn_on_duplicate_tools: bool = True): 22 | self._tools: dict[str, Tool] = {} 23 | self.warn_on_duplicate_tools = warn_on_duplicate_tools 24 | 25 | def get_tool(self, name: str) -> Tool | None: 26 | """Get tool by name.""" 27 | return self._tools.get(name) 28 | 29 | def list_tools(self) -> list[Tool]: 30 | """List all registered tools.""" 31 | return list(self._tools.values()) 32 | 33 | def add_tool( 34 | self, 35 | fn: Callable[..., Any], 36 | name: str | None = None, 37 | description: str | None = None, 38 | ) -> Tool: 39 | """Add a tool to the server.""" 40 | tool = Tool.from_function(fn, name=name, description=description) 41 | existing = self._tools.get(tool.name) 42 | if existing: 43 | if self.warn_on_duplicate_tools: 44 | logger.warning(f"Tool already exists: {tool.name}") 45 | return existing 46 | self._tools[tool.name] = tool 47 | return tool 48 | 49 | async def call_tool( 50 | self, 51 | name: str, 52 | arguments: dict[str, Any], 53 | context: Context[ServerSessionT, LifespanContextT] | None = None, 54 | ) -> Any: 55 | """Call a tool by name with arguments.""" 56 | tool = self.get_tool(name) 57 | if not tool: 58 | raise ToolError(f"Unknown tool: {name}") 59 | 60 | return await tool.run(arguments, context=context) 61 | -------------------------------------------------------------------------------- /src/mcp/server/fastmcp/utilities/__init__.py: -------------------------------------------------------------------------------- 1 | """FastMCP utility modules.""" 2 | -------------------------------------------------------------------------------- /src/mcp/server/fastmcp/utilities/logging.py: -------------------------------------------------------------------------------- 1 | """Logging utilities for FastMCP.""" 2 | 3 | import logging 4 | from typing import Literal 5 | 6 | 7 | def get_logger(name: str) -> logging.Logger: 8 | """Get a logger nested under MCPnamespace. 9 | 10 | Args: 11 | name: the name of the logger, which will be prefixed with 'FastMCP.' 12 | 13 | Returns: 14 | a configured logger instance 15 | """ 16 | return logging.getLogger(name) 17 | 18 | 19 | def configure_logging( 20 | level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO", 21 | ) -> None: 22 | """Configure logging for MCP. 23 | 24 | Args: 25 | level: the log level to use 26 | """ 27 | handlers: list[logging.Handler] = [] 28 | try: 29 | from rich.console import Console 30 | from rich.logging import RichHandler 31 | 32 | handlers.append(RichHandler(console=Console(stderr=True), rich_tracebacks=True)) 33 | except ImportError: 34 | pass 35 | 36 | if not handlers: 37 | handlers.append(logging.StreamHandler()) 38 | 39 | logging.basicConfig( 40 | level=level, 41 | format="%(message)s", 42 | handlers=handlers, 43 | ) 44 | -------------------------------------------------------------------------------- /src/mcp/server/fastmcp/utilities/types.py: -------------------------------------------------------------------------------- 1 | """Common types used across FastMCP.""" 2 | 3 | import base64 4 | from pathlib import Path 5 | 6 | from mcp.types import ImageContent 7 | 8 | 9 | class Image: 10 | """Helper class for returning images from tools.""" 11 | 12 | def __init__( 13 | self, 14 | path: str | Path | None = None, 15 | data: bytes | None = None, 16 | format: str | None = None, 17 | ): 18 | if path is None and data is None: 19 | raise ValueError("Either path or data must be provided") 20 | if path is not None and data is not None: 21 | raise ValueError("Only one of path or data can be provided") 22 | 23 | self.path = Path(path) if path else None 24 | self.data = data 25 | self._format = format 26 | self._mime_type = self._get_mime_type() 27 | 28 | def _get_mime_type(self) -> str: 29 | """Get MIME type from format or guess from file extension.""" 30 | if self._format: 31 | return f"image/{self._format.lower()}" 32 | 33 | if self.path: 34 | suffix = self.path.suffix.lower() 35 | return { 36 | ".png": "image/png", 37 | ".jpg": "image/jpeg", 38 | ".jpeg": "image/jpeg", 39 | ".gif": "image/gif", 40 | ".webp": "image/webp", 41 | }.get(suffix, "application/octet-stream") 42 | return "image/png" # default for raw binary data 43 | 44 | def to_image_content(self) -> ImageContent: 45 | """Convert to MCP ImageContent.""" 46 | if self.path: 47 | with open(self.path, "rb") as f: 48 | data = base64.b64encode(f.read()).decode() 49 | elif self.data is not None: 50 | data = base64.b64encode(self.data).decode() 51 | else: 52 | raise ValueError("No image data available") 53 | 54 | return ImageContent(type="image", data=data, mimeType=self._mime_type) 55 | -------------------------------------------------------------------------------- /src/mcp/server/lowlevel/__init__.py: -------------------------------------------------------------------------------- 1 | from .server import NotificationOptions, Server 2 | 3 | __all__ = ["Server", "NotificationOptions"] 4 | -------------------------------------------------------------------------------- /src/mcp/server/lowlevel/helper_types.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass 5 | class ReadResourceContents: 6 | """Contents returned from a read_resource call.""" 7 | 8 | content: str | bytes 9 | mime_type: str | None = None 10 | -------------------------------------------------------------------------------- /src/mcp/server/models.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module provides simpler types to use with the server for managing prompts 3 | and tools. 4 | """ 5 | 6 | from pydantic import BaseModel 7 | 8 | from mcp.types import ( 9 | ServerCapabilities, 10 | ) 11 | 12 | 13 | class InitializationOptions(BaseModel): 14 | server_name: str 15 | server_version: str 16 | capabilities: ServerCapabilities 17 | instructions: str | None = None 18 | -------------------------------------------------------------------------------- /src/mcp/server/stdio.py: -------------------------------------------------------------------------------- 1 | """ 2 | Stdio Server Transport Module 3 | 4 | This module provides functionality for creating an stdio-based transport layer 5 | that can be used to communicate with an MCP client through standard input/output 6 | streams. 7 | 8 | Example usage: 9 | ``` 10 | async def run_server(): 11 | async with stdio_server() as (read_stream, write_stream): 12 | # read_stream contains incoming JSONRPCMessages from stdin 13 | # write_stream allows sending JSONRPCMessages to stdout 14 | server = await create_my_server() 15 | await server.run(read_stream, write_stream, init_options) 16 | 17 | anyio.run(run_server) 18 | ``` 19 | """ 20 | 21 | import sys 22 | from contextlib import asynccontextmanager 23 | from io import TextIOWrapper 24 | 25 | import anyio 26 | import anyio.lowlevel 27 | from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream 28 | 29 | import mcp.types as types 30 | 31 | 32 | @asynccontextmanager 33 | async def stdio_server( 34 | stdin: anyio.AsyncFile[str] | None = None, 35 | stdout: anyio.AsyncFile[str] | None = None, 36 | ): 37 | """ 38 | Server transport for stdio: this communicates with an MCP client by reading 39 | from the current process' stdin and writing to stdout. 40 | """ 41 | # Purposely not using context managers for these, as we don't want to close 42 | # standard process handles. Encoding of stdin/stdout as text streams on 43 | # python is platform-dependent (Windows is particularly problematic), so we 44 | # re-wrap the underlying binary stream to ensure UTF-8. 45 | if not stdin: 46 | stdin = anyio.wrap_file(TextIOWrapper(sys.stdin.buffer, encoding="utf-8")) 47 | if not stdout: 48 | stdout = anyio.wrap_file(TextIOWrapper(sys.stdout.buffer, encoding="utf-8")) 49 | 50 | read_stream: MemoryObjectReceiveStream[types.JSONRPCMessage | Exception] 51 | read_stream_writer: MemoryObjectSendStream[types.JSONRPCMessage | Exception] 52 | 53 | write_stream: MemoryObjectSendStream[types.JSONRPCMessage] 54 | write_stream_reader: MemoryObjectReceiveStream[types.JSONRPCMessage] 55 | 56 | read_stream_writer, read_stream = anyio.create_memory_object_stream(0) 57 | write_stream, write_stream_reader = anyio.create_memory_object_stream(0) 58 | 59 | async def stdin_reader(): 60 | try: 61 | async with read_stream_writer: 62 | async for line in stdin: 63 | try: 64 | message = types.JSONRPCMessage.model_validate_json(line) 65 | except Exception as exc: 66 | await read_stream_writer.send(exc) 67 | continue 68 | 69 | await read_stream_writer.send(message) 70 | except anyio.ClosedResourceError: 71 | await anyio.lowlevel.checkpoint() 72 | 73 | async def stdout_writer(): 74 | try: 75 | async with write_stream_reader: 76 | async for message in write_stream_reader: 77 | json = message.model_dump_json(by_alias=True, exclude_none=True) 78 | await stdout.write(json + "\n") 79 | await stdout.flush() 80 | except anyio.ClosedResourceError: 81 | await anyio.lowlevel.checkpoint() 82 | 83 | async with anyio.create_task_group() as tg: 84 | tg.start_soon(stdin_reader) 85 | tg.start_soon(stdout_writer) 86 | yield read_stream, write_stream 87 | -------------------------------------------------------------------------------- /src/mcp/server/websocket.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from contextlib import asynccontextmanager 3 | 4 | import anyio 5 | from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream 6 | from pydantic_core import ValidationError 7 | from starlette.types import Receive, Scope, Send 8 | from starlette.websockets import WebSocket 9 | 10 | import mcp.types as types 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | @asynccontextmanager 16 | async def websocket_server(scope: Scope, receive: Receive, send: Send): 17 | """ 18 | WebSocket server transport for MCP. This is an ASGI application, suitable to be 19 | used with a framework like Starlette and a server like Hypercorn. 20 | """ 21 | 22 | websocket = WebSocket(scope, receive, send) 23 | await websocket.accept(subprotocol="mcp") 24 | 25 | read_stream: MemoryObjectReceiveStream[types.JSONRPCMessage | Exception] 26 | read_stream_writer: MemoryObjectSendStream[types.JSONRPCMessage | Exception] 27 | 28 | write_stream: MemoryObjectSendStream[types.JSONRPCMessage] 29 | write_stream_reader: MemoryObjectReceiveStream[types.JSONRPCMessage] 30 | 31 | read_stream_writer, read_stream = anyio.create_memory_object_stream(0) 32 | write_stream, write_stream_reader = anyio.create_memory_object_stream(0) 33 | 34 | async def ws_reader(): 35 | try: 36 | async with read_stream_writer: 37 | async for msg in websocket.iter_text(): 38 | try: 39 | client_message = types.JSONRPCMessage.model_validate_json(msg) 40 | except ValidationError as exc: 41 | await read_stream_writer.send(exc) 42 | continue 43 | 44 | await read_stream_writer.send(client_message) 45 | except anyio.ClosedResourceError: 46 | await websocket.close() 47 | 48 | async def ws_writer(): 49 | try: 50 | async with write_stream_reader: 51 | async for message in write_stream_reader: 52 | obj = message.model_dump_json(by_alias=True, exclude_none=True) 53 | await websocket.send_text(obj) 54 | except anyio.ClosedResourceError: 55 | await websocket.close() 56 | 57 | async with anyio.create_task_group() as tg: 58 | tg.start_soon(ws_reader) 59 | tg.start_soon(ws_writer) 60 | yield (read_stream, write_stream) 61 | -------------------------------------------------------------------------------- /src/mcp/shared/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/b4c7db6a50a5c88bae1db5c1f7fba44d16eebc6e/src/mcp/shared/__init__.py -------------------------------------------------------------------------------- /src/mcp/shared/context.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Any, Generic 3 | 4 | from typing_extensions import TypeVar 5 | 6 | from mcp.shared.session import BaseSession 7 | from mcp.types import RequestId, RequestParams 8 | 9 | SessionT = TypeVar("SessionT", bound=BaseSession[Any, Any, Any, Any, Any]) 10 | LifespanContextT = TypeVar("LifespanContextT") 11 | 12 | 13 | @dataclass 14 | class RequestContext(Generic[SessionT, LifespanContextT]): 15 | request_id: RequestId 16 | meta: RequestParams.Meta | None 17 | session: SessionT 18 | lifespan_context: LifespanContextT 19 | -------------------------------------------------------------------------------- /src/mcp/shared/exceptions.py: -------------------------------------------------------------------------------- 1 | from mcp.types import ErrorData 2 | 3 | 4 | class McpError(Exception): 5 | """ 6 | Exception type raised when an error arrives over an MCP connection. 7 | """ 8 | 9 | error: ErrorData 10 | 11 | def __init__(self, error: ErrorData): 12 | """Initialize McpError.""" 13 | super().__init__(error.message) 14 | self.error = error 15 | -------------------------------------------------------------------------------- /src/mcp/shared/memory.py: -------------------------------------------------------------------------------- 1 | """ 2 | In-memory transports 3 | """ 4 | 5 | from collections.abc import AsyncGenerator 6 | from contextlib import asynccontextmanager 7 | from datetime import timedelta 8 | from typing import Any 9 | 10 | import anyio 11 | from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream 12 | 13 | import mcp.types as types 14 | from mcp.client.session import ( 15 | ClientSession, 16 | ListRootsFnT, 17 | LoggingFnT, 18 | MessageHandlerFnT, 19 | SamplingFnT, 20 | ) 21 | from mcp.server import Server 22 | from mcp.types import JSONRPCMessage 23 | 24 | MessageStream = tuple[ 25 | MemoryObjectReceiveStream[JSONRPCMessage | Exception], 26 | MemoryObjectSendStream[JSONRPCMessage], 27 | ] 28 | 29 | 30 | @asynccontextmanager 31 | async def create_client_server_memory_streams() -> ( 32 | AsyncGenerator[tuple[MessageStream, MessageStream], None] 33 | ): 34 | """ 35 | Creates a pair of bidirectional memory streams for client-server communication. 36 | 37 | Returns: 38 | A tuple of (client_streams, server_streams) where each is a tuple of 39 | (read_stream, write_stream) 40 | """ 41 | # Create streams for both directions 42 | server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[ 43 | JSONRPCMessage | Exception 44 | ](1) 45 | client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[ 46 | JSONRPCMessage | Exception 47 | ](1) 48 | 49 | client_streams = (server_to_client_receive, client_to_server_send) 50 | server_streams = (client_to_server_receive, server_to_client_send) 51 | 52 | async with ( 53 | server_to_client_receive, 54 | client_to_server_send, 55 | client_to_server_receive, 56 | server_to_client_send, 57 | ): 58 | yield client_streams, server_streams 59 | 60 | 61 | @asynccontextmanager 62 | async def create_connected_server_and_client_session( 63 | server: Server[Any], 64 | read_timeout_seconds: timedelta | None = None, 65 | sampling_callback: SamplingFnT | None = None, 66 | list_roots_callback: ListRootsFnT | None = None, 67 | logging_callback: LoggingFnT | None = None, 68 | message_handler: MessageHandlerFnT | None = None, 69 | client_info: types.Implementation | None = None, 70 | raise_exceptions: bool = False, 71 | ) -> AsyncGenerator[ClientSession, None]: 72 | """Creates a ClientSession that is connected to a running MCP server.""" 73 | async with create_client_server_memory_streams() as ( 74 | client_streams, 75 | server_streams, 76 | ): 77 | client_read, client_write = client_streams 78 | server_read, server_write = server_streams 79 | 80 | # Create a cancel scope for the server task 81 | async with anyio.create_task_group() as tg: 82 | tg.start_soon( 83 | lambda: server.run( 84 | server_read, 85 | server_write, 86 | server.create_initialization_options(), 87 | raise_exceptions=raise_exceptions, 88 | ) 89 | ) 90 | 91 | try: 92 | async with ClientSession( 93 | read_stream=client_read, 94 | write_stream=client_write, 95 | read_timeout_seconds=read_timeout_seconds, 96 | sampling_callback=sampling_callback, 97 | list_roots_callback=list_roots_callback, 98 | logging_callback=logging_callback, 99 | message_handler=message_handler, 100 | client_info=client_info, 101 | ) as client_session: 102 | await client_session.initialize() 103 | yield client_session 104 | finally: 105 | tg.cancel_scope.cancel() 106 | -------------------------------------------------------------------------------- /src/mcp/shared/progress.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Generator 2 | from contextlib import contextmanager 3 | from dataclasses import dataclass, field 4 | from typing import Generic 5 | 6 | from pydantic import BaseModel 7 | 8 | from mcp.shared.context import LifespanContextT, RequestContext 9 | from mcp.shared.session import ( 10 | BaseSession, 11 | ReceiveNotificationT, 12 | ReceiveRequestT, 13 | SendNotificationT, 14 | SendRequestT, 15 | SendResultT, 16 | ) 17 | from mcp.types import ProgressToken 18 | 19 | 20 | class Progress(BaseModel): 21 | progress: float 22 | total: float | None 23 | 24 | 25 | @dataclass 26 | class ProgressContext( 27 | Generic[ 28 | SendRequestT, 29 | SendNotificationT, 30 | SendResultT, 31 | ReceiveRequestT, 32 | ReceiveNotificationT, 33 | ] 34 | ): 35 | session: BaseSession[ 36 | SendRequestT, 37 | SendNotificationT, 38 | SendResultT, 39 | ReceiveRequestT, 40 | ReceiveNotificationT, 41 | ] 42 | progress_token: ProgressToken 43 | total: float | None 44 | current: float = field(default=0.0, init=False) 45 | 46 | async def progress(self, amount: float) -> None: 47 | self.current += amount 48 | 49 | await self.session.send_progress_notification( 50 | self.progress_token, self.current, total=self.total 51 | ) 52 | 53 | 54 | @contextmanager 55 | def progress( 56 | ctx: RequestContext[ 57 | BaseSession[ 58 | SendRequestT, 59 | SendNotificationT, 60 | SendResultT, 61 | ReceiveRequestT, 62 | ReceiveNotificationT, 63 | ], 64 | LifespanContextT, 65 | ], 66 | total: float | None = None, 67 | ) -> Generator[ 68 | ProgressContext[ 69 | SendRequestT, 70 | SendNotificationT, 71 | SendResultT, 72 | ReceiveRequestT, 73 | ReceiveNotificationT, 74 | ], 75 | None, 76 | ]: 77 | if ctx.meta is None or ctx.meta.progressToken is None: 78 | raise ValueError("No progress token provided") 79 | 80 | progress_ctx = ProgressContext(ctx.session, ctx.meta.progressToken, total) 81 | try: 82 | yield progress_ctx 83 | finally: 84 | pass 85 | -------------------------------------------------------------------------------- /src/mcp/shared/version.py: -------------------------------------------------------------------------------- 1 | from mcp.types import LATEST_PROTOCOL_VERSION 2 | 3 | SUPPORTED_PROTOCOL_VERSIONS: tuple[int, str] = (1, LATEST_PROTOCOL_VERSION) 4 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/b4c7db6a50a5c88bae1db5c1f7fba44d16eebc6e/tests/__init__.py -------------------------------------------------------------------------------- /tests/client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/b4c7db6a50a5c88bae1db5c1f7fba44d16eebc6e/tests/client/__init__.py -------------------------------------------------------------------------------- /tests/client/test_config.py: -------------------------------------------------------------------------------- 1 | import json 2 | import subprocess 3 | from pathlib import Path 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from mcp.cli.claude import update_claude_config 9 | 10 | 11 | @pytest.fixture 12 | def temp_config_dir(tmp_path: Path): 13 | """Create a temporary Claude config directory.""" 14 | config_dir = tmp_path / "Claude" 15 | config_dir.mkdir() 16 | return config_dir 17 | 18 | 19 | @pytest.fixture 20 | def mock_config_path(temp_config_dir: Path): 21 | """Mock get_claude_config_path to return our temporary directory.""" 22 | with patch("mcp.cli.claude.get_claude_config_path", return_value=temp_config_dir): 23 | yield temp_config_dir 24 | 25 | 26 | def test_command_execution(mock_config_path: Path): 27 | """Test that the generated command can actually be executed.""" 28 | # Setup 29 | server_name = "test_server" 30 | file_spec = "test_server.py:app" 31 | 32 | # Update config 33 | success = update_claude_config(file_spec=file_spec, server_name=server_name) 34 | assert success 35 | 36 | # Read the generated config 37 | config_file = mock_config_path / "claude_desktop_config.json" 38 | config = json.loads(config_file.read_text()) 39 | 40 | # Get the command and args 41 | server_config = config["mcpServers"][server_name] 42 | command = server_config["command"] 43 | args = server_config["args"] 44 | 45 | test_args = [command] + args + ["--help"] 46 | 47 | result = subprocess.run(test_args, capture_output=True, text=True, timeout=5) 48 | 49 | assert result.returncode == 0 50 | assert "usage" in result.stdout.lower() 51 | -------------------------------------------------------------------------------- /tests/client/test_list_roots_callback.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pydantic import FileUrl 3 | 4 | from mcp.client.session import ClientSession 5 | from mcp.server.fastmcp.server import Context 6 | from mcp.shared.context import RequestContext 7 | from mcp.shared.memory import ( 8 | create_connected_server_and_client_session as create_session, 9 | ) 10 | from mcp.types import ListRootsResult, Root, TextContent 11 | 12 | 13 | @pytest.mark.anyio 14 | async def test_list_roots_callback(): 15 | from mcp.server.fastmcp import FastMCP 16 | 17 | server = FastMCP("test") 18 | 19 | callback_return = ListRootsResult( 20 | roots=[ 21 | Root( 22 | uri=FileUrl("file://users/fake/test"), 23 | name="Test Root 1", 24 | ), 25 | Root( 26 | uri=FileUrl("file://users/fake/test/2"), 27 | name="Test Root 2", 28 | ), 29 | ] 30 | ) 31 | 32 | async def list_roots_callback( 33 | context: RequestContext[ClientSession, None], 34 | ) -> ListRootsResult: 35 | return callback_return 36 | 37 | @server.tool("test_list_roots") 38 | async def test_list_roots(context: Context, message: str): # type: ignore[reportUnknownMemberType] 39 | roots = await context.session.list_roots() 40 | assert roots == callback_return 41 | return True 42 | 43 | # Test with list_roots callback 44 | async with create_session( 45 | server._mcp_server, list_roots_callback=list_roots_callback 46 | ) as client_session: 47 | # Make a request to trigger sampling callback 48 | result = await client_session.call_tool( 49 | "test_list_roots", {"message": "test message"} 50 | ) 51 | assert result.isError is False 52 | assert isinstance(result.content[0], TextContent) 53 | assert result.content[0].text == "true" 54 | 55 | # Test without list_roots callback 56 | async with create_session(server._mcp_server) as client_session: 57 | # Make a request to trigger sampling callback 58 | result = await client_session.call_tool( 59 | "test_list_roots", {"message": "test message"} 60 | ) 61 | assert result.isError is True 62 | assert isinstance(result.content[0], TextContent) 63 | assert ( 64 | result.content[0].text 65 | == "Error executing tool test_list_roots: List roots not supported" 66 | ) 67 | -------------------------------------------------------------------------------- /tests/client/test_logging_callback.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | 3 | import pytest 4 | 5 | import mcp.types as types 6 | from mcp.shared.memory import ( 7 | create_connected_server_and_client_session as create_session, 8 | ) 9 | from mcp.shared.session import RequestResponder 10 | from mcp.types import ( 11 | LoggingMessageNotificationParams, 12 | TextContent, 13 | ) 14 | 15 | 16 | class LoggingCollector: 17 | def __init__(self): 18 | self.log_messages: list[LoggingMessageNotificationParams] = [] 19 | 20 | async def __call__(self, params: LoggingMessageNotificationParams) -> None: 21 | self.log_messages.append(params) 22 | 23 | 24 | @pytest.mark.anyio 25 | async def test_logging_callback(): 26 | from mcp.server.fastmcp import FastMCP 27 | 28 | server = FastMCP("test") 29 | logging_collector = LoggingCollector() 30 | 31 | # Create a simple test tool 32 | @server.tool("test_tool") 33 | async def test_tool() -> bool: 34 | # The actual tool is very simple and just returns True 35 | return True 36 | 37 | # Create a function that can send a log notification 38 | @server.tool("test_tool_with_log") 39 | async def test_tool_with_log( 40 | message: str, level: Literal["debug", "info", "warning", "error"], logger: str 41 | ) -> bool: 42 | """Send a log notification to the client.""" 43 | await server.get_context().log( 44 | level=level, 45 | message=message, 46 | logger_name=logger, 47 | ) 48 | return True 49 | 50 | # Create a message handler to catch exceptions 51 | async def message_handler( 52 | message: RequestResponder[types.ServerRequest, types.ClientResult] 53 | | types.ServerNotification 54 | | Exception, 55 | ) -> None: 56 | if isinstance(message, Exception): 57 | raise message 58 | 59 | async with create_session( 60 | server._mcp_server, 61 | logging_callback=logging_collector, 62 | message_handler=message_handler, 63 | ) as client_session: 64 | # First verify our test tool works 65 | result = await client_session.call_tool("test_tool", {}) 66 | assert result.isError is False 67 | assert isinstance(result.content[0], TextContent) 68 | assert result.content[0].text == "true" 69 | 70 | # Now send a log message via our tool 71 | log_result = await client_session.call_tool( 72 | "test_tool_with_log", 73 | { 74 | "message": "Test log message", 75 | "level": "info", 76 | "logger": "test_logger", 77 | }, 78 | ) 79 | assert log_result.isError is False 80 | assert len(logging_collector.log_messages) == 1 81 | assert logging_collector.log_messages[0] == LoggingMessageNotificationParams( 82 | level="info", logger="test_logger", data="Test log message" 83 | ) 84 | -------------------------------------------------------------------------------- /tests/client/test_sampling_callback.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from mcp.client.session import ClientSession 4 | from mcp.shared.context import RequestContext 5 | from mcp.shared.memory import ( 6 | create_connected_server_and_client_session as create_session, 7 | ) 8 | from mcp.types import ( 9 | CreateMessageRequestParams, 10 | CreateMessageResult, 11 | SamplingMessage, 12 | TextContent, 13 | ) 14 | 15 | 16 | @pytest.mark.anyio 17 | async def test_sampling_callback(): 18 | from mcp.server.fastmcp import FastMCP 19 | 20 | server = FastMCP("test") 21 | 22 | callback_return = CreateMessageResult( 23 | role="assistant", 24 | content=TextContent( 25 | type="text", text="This is a response from the sampling callback" 26 | ), 27 | model="test-model", 28 | stopReason="endTurn", 29 | ) 30 | 31 | async def sampling_callback( 32 | context: RequestContext[ClientSession, None], 33 | params: CreateMessageRequestParams, 34 | ) -> CreateMessageResult: 35 | return callback_return 36 | 37 | @server.tool("test_sampling") 38 | async def test_sampling_tool(message: str): 39 | value = await server.get_context().session.create_message( 40 | messages=[ 41 | SamplingMessage( 42 | role="user", content=TextContent(type="text", text=message) 43 | ) 44 | ], 45 | max_tokens=100, 46 | ) 47 | assert value == callback_return 48 | return True 49 | 50 | # Test with sampling callback 51 | async with create_session( 52 | server._mcp_server, sampling_callback=sampling_callback 53 | ) as client_session: 54 | # Make a request to trigger sampling callback 55 | result = await client_session.call_tool( 56 | "test_sampling", {"message": "Test message for sampling"} 57 | ) 58 | assert result.isError is False 59 | assert isinstance(result.content[0], TextContent) 60 | assert result.content[0].text == "true" 61 | 62 | # Test without sampling callback 63 | async with create_session(server._mcp_server) as client_session: 64 | # Make a request to trigger sampling callback 65 | result = await client_session.call_tool( 66 | "test_sampling", {"message": "Test message for sampling"} 67 | ) 68 | assert result.isError is True 69 | assert isinstance(result.content[0], TextContent) 70 | assert ( 71 | result.content[0].text 72 | == "Error executing tool test_sampling: Sampling not supported" 73 | ) 74 | -------------------------------------------------------------------------------- /tests/client/test_stdio.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | 3 | import pytest 4 | 5 | from mcp.client.stdio import StdioServerParameters, stdio_client 6 | from mcp.types import JSONRPCMessage, JSONRPCRequest, JSONRPCResponse 7 | 8 | tee: str = shutil.which("tee") # type: ignore 9 | 10 | 11 | @pytest.mark.anyio 12 | @pytest.mark.skipif(tee is None, reason="could not find tee command") 13 | async def test_stdio_client(): 14 | server_parameters = StdioServerParameters(command=tee) 15 | 16 | async with stdio_client(server_parameters) as (read_stream, write_stream): 17 | # Test sending and receiving messages 18 | messages = [ 19 | JSONRPCMessage(root=JSONRPCRequest(jsonrpc="2.0", id=1, method="ping")), 20 | JSONRPCMessage(root=JSONRPCResponse(jsonrpc="2.0", id=2, result={})), 21 | ] 22 | 23 | async with write_stream: 24 | for message in messages: 25 | await write_stream.send(message) 26 | 27 | read_messages = [] 28 | async with read_stream: 29 | async for message in read_stream: 30 | if isinstance(message, Exception): 31 | raise message 32 | 33 | read_messages.append(message) 34 | if len(read_messages) == 2: 35 | break 36 | 37 | assert len(read_messages) == 2 38 | assert read_messages[0] == JSONRPCMessage( 39 | root=JSONRPCRequest(jsonrpc="2.0", id=1, method="ping") 40 | ) 41 | assert read_messages[1] == JSONRPCMessage( 42 | root=JSONRPCResponse(jsonrpc="2.0", id=2, result={}) 43 | ) 44 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture 5 | def anyio_backend(): 6 | return "asyncio" 7 | -------------------------------------------------------------------------------- /tests/issues/test_100_tool_listing.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from mcp.server.fastmcp import FastMCP 4 | 5 | pytestmark = pytest.mark.anyio 6 | 7 | 8 | async def test_list_tools_returns_all_tools(): 9 | mcp = FastMCP("TestTools") 10 | 11 | # Create 100 tools with unique names 12 | num_tools = 100 13 | for i in range(num_tools): 14 | 15 | @mcp.tool(name=f"tool_{i}") 16 | def dummy_tool_func(): 17 | f"""Tool number {i}""" 18 | return i 19 | 20 | globals()[f"dummy_tool_{i}"] = ( 21 | dummy_tool_func # Keep reference to avoid garbage collection 22 | ) 23 | 24 | # Get all tools 25 | tools = await mcp.list_tools() 26 | 27 | # Verify we get all tools 28 | assert len(tools) == num_tools, f"Expected {num_tools} tools, but got {len(tools)}" 29 | 30 | # Verify each tool is unique and has the correct name 31 | tool_names = [tool.name for tool in tools] 32 | expected_names = [f"tool_{i}" for i in range(num_tools)] 33 | assert sorted(tool_names) == sorted( 34 | expected_names 35 | ), "Tool names don't match expected names" 36 | -------------------------------------------------------------------------------- /tests/issues/test_129_resource_templates.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from mcp import types 4 | from mcp.server.fastmcp import FastMCP 5 | 6 | 7 | @pytest.mark.anyio 8 | async def test_resource_templates(): 9 | # Create an MCP server 10 | mcp = FastMCP("Demo") 11 | 12 | # Add a dynamic greeting resource 13 | @mcp.resource("greeting://{name}") 14 | def get_greeting(name: str) -> str: 15 | """Get a personalized greeting""" 16 | return f"Hello, {name}!" 17 | 18 | @mcp.resource("users://{user_id}/profile") 19 | def get_user_profile(user_id: str) -> str: 20 | """Dynamic user data""" 21 | return f"Profile data for user {user_id}" 22 | 23 | # Get the list of resource templates using the underlying server 24 | # Note: list_resource_templates() returns a decorator that wraps the handler 25 | # The handler returns a ServerResult with a ListResourceTemplatesResult inside 26 | result = await mcp._mcp_server.request_handlers[types.ListResourceTemplatesRequest]( 27 | types.ListResourceTemplatesRequest( 28 | method="resources/templates/list", params=None, cursor=None 29 | ) 30 | ) 31 | assert isinstance(result.root, types.ListResourceTemplatesResult) 32 | templates = result.root.resourceTemplates 33 | 34 | # Verify we get both templates back 35 | assert len(templates) == 2 36 | 37 | # Verify template details 38 | greeting_template = next(t for t in templates if t.name == "get_greeting") 39 | assert greeting_template.uriTemplate == "greeting://{name}" 40 | assert greeting_template.description == "Get a personalized greeting" 41 | 42 | profile_template = next(t for t in templates if t.name == "get_user_profile") 43 | assert profile_template.uriTemplate == "users://{user_id}/profile" 44 | assert profile_template.description == "Dynamic user data" 45 | -------------------------------------------------------------------------------- /tests/issues/test_141_resource_templates.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pydantic import AnyUrl 3 | 4 | from mcp.server.fastmcp import FastMCP 5 | from mcp.shared.memory import ( 6 | create_connected_server_and_client_session as client_session, 7 | ) 8 | from mcp.types import ( 9 | ListResourceTemplatesResult, 10 | TextResourceContents, 11 | ) 12 | 13 | 14 | @pytest.mark.anyio 15 | async def test_resource_template_edge_cases(): 16 | """Test server-side resource template validation""" 17 | mcp = FastMCP("Demo") 18 | 19 | # Test case 1: Template with multiple parameters 20 | @mcp.resource("resource://users/{user_id}/posts/{post_id}") 21 | def get_user_post(user_id: str, post_id: str) -> str: 22 | return f"Post {post_id} by user {user_id}" 23 | 24 | # Test case 2: Template with optional parameter (should fail) 25 | with pytest.raises(ValueError, match="Mismatch between URI parameters"): 26 | 27 | @mcp.resource("resource://users/{user_id}/profile") 28 | def get_user_profile(user_id: str, optional_param: str | None = None) -> str: 29 | return f"Profile for user {user_id}" 30 | 31 | # Test case 3: Template with mismatched parameters 32 | with pytest.raises(ValueError, match="Mismatch between URI parameters"): 33 | 34 | @mcp.resource("resource://users/{user_id}/profile") 35 | def get_user_profile_mismatch(different_param: str) -> str: 36 | return f"Profile for user {different_param}" 37 | 38 | # Test case 4: Template with extra function parameters 39 | with pytest.raises(ValueError, match="Mismatch between URI parameters"): 40 | 41 | @mcp.resource("resource://users/{user_id}/profile") 42 | def get_user_profile_extra(user_id: str, extra_param: str) -> str: 43 | return f"Profile for user {user_id}" 44 | 45 | # Test case 5: Template with missing function parameters 46 | with pytest.raises(ValueError, match="Mismatch between URI parameters"): 47 | 48 | @mcp.resource("resource://users/{user_id}/profile/{section}") 49 | def get_user_profile_missing(user_id: str) -> str: 50 | return f"Profile for user {user_id}" 51 | 52 | # Verify valid template works 53 | result = await mcp.read_resource("resource://users/123/posts/456") 54 | result_list = list(result) 55 | assert len(result_list) == 1 56 | assert result_list[0].content == "Post 456 by user 123" 57 | assert result_list[0].mime_type == "text/plain" 58 | 59 | # Verify invalid parameters raise error 60 | with pytest.raises(ValueError, match="Unknown resource"): 61 | await mcp.read_resource("resource://users/123/posts") # Missing post_id 62 | 63 | with pytest.raises(ValueError, match="Unknown resource"): 64 | await mcp.read_resource( 65 | "resource://users/123/posts/456/extra" 66 | ) # Extra path component 67 | 68 | 69 | @pytest.mark.anyio 70 | async def test_resource_template_client_interaction(): 71 | """Test client-side resource template interaction""" 72 | mcp = FastMCP("Demo") 73 | 74 | # Register some templated resources 75 | @mcp.resource("resource://users/{user_id}/posts/{post_id}") 76 | def get_user_post(user_id: str, post_id: str) -> str: 77 | return f"Post {post_id} by user {user_id}" 78 | 79 | @mcp.resource("resource://users/{user_id}/profile") 80 | def get_user_profile(user_id: str) -> str: 81 | return f"Profile for user {user_id}" 82 | 83 | async with client_session(mcp._mcp_server) as session: 84 | # Initialize the session 85 | await session.initialize() 86 | 87 | # List available resources 88 | resources = await session.list_resource_templates() 89 | assert isinstance(resources, ListResourceTemplatesResult) 90 | assert len(resources.resourceTemplates) == 2 91 | 92 | # Verify resource templates are listed correctly 93 | templates = [r.uriTemplate for r in resources.resourceTemplates] 94 | assert "resource://users/{user_id}/posts/{post_id}" in templates 95 | assert "resource://users/{user_id}/profile" in templates 96 | 97 | # Read a resource with valid parameters 98 | result = await session.read_resource(AnyUrl("resource://users/123/posts/456")) 99 | contents = result.contents[0] 100 | assert isinstance(contents, TextResourceContents) 101 | assert contents.text == "Post 456 by user 123" 102 | assert contents.mimeType == "text/plain" 103 | 104 | # Read another resource with valid parameters 105 | result = await session.read_resource(AnyUrl("resource://users/789/profile")) 106 | contents = result.contents[0] 107 | assert isinstance(contents, TextResourceContents) 108 | assert contents.text == "Profile for user 789" 109 | assert contents.mimeType == "text/plain" 110 | 111 | # Verify invalid resource URIs raise appropriate errors 112 | with pytest.raises(Exception): # Specific exception type may vary 113 | await session.read_resource( 114 | AnyUrl("resource://users/123/posts") 115 | ) # Missing post_id 116 | 117 | with pytest.raises(Exception): # Specific exception type may vary 118 | await session.read_resource( 119 | AnyUrl("resource://users/123/invalid") 120 | ) # Invalid template 121 | -------------------------------------------------------------------------------- /tests/issues/test_152_resource_mime_type.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | import pytest 4 | from pydantic import AnyUrl 5 | 6 | from mcp import types 7 | from mcp.server.fastmcp import FastMCP 8 | from mcp.server.lowlevel import Server 9 | from mcp.server.lowlevel.helper_types import ReadResourceContents 10 | from mcp.shared.memory import ( 11 | create_connected_server_and_client_session as client_session, 12 | ) 13 | 14 | pytestmark = pytest.mark.anyio 15 | 16 | 17 | async def test_fastmcp_resource_mime_type(): 18 | """Test that mime_type parameter is respected for resources.""" 19 | mcp = FastMCP("test") 20 | 21 | # Create a small test image as bytes 22 | image_bytes = b"fake_image_data" 23 | base64_string = base64.b64encode(image_bytes).decode("utf-8") 24 | 25 | @mcp.resource("test://image", mime_type="image/png") 26 | def get_image_as_string() -> str: 27 | """Return a test image as base64 string.""" 28 | return base64_string 29 | 30 | @mcp.resource("test://image_bytes", mime_type="image/png") 31 | def get_image_as_bytes() -> bytes: 32 | """Return a test image as bytes.""" 33 | return image_bytes 34 | 35 | # Test that resources are listed with correct mime type 36 | async with client_session(mcp._mcp_server) as client: 37 | # List resources and verify mime types 38 | resources = await client.list_resources() 39 | assert resources.resources is not None 40 | 41 | mapping = {str(r.uri): r for r in resources.resources} 42 | 43 | # Find our resources 44 | string_resource = mapping["test://image"] 45 | bytes_resource = mapping["test://image_bytes"] 46 | 47 | # Verify mime types 48 | assert ( 49 | string_resource.mimeType == "image/png" 50 | ), "String resource mime type not respected" 51 | assert ( 52 | bytes_resource.mimeType == "image/png" 53 | ), "Bytes resource mime type not respected" 54 | 55 | # Also verify the content can be read correctly 56 | string_result = await client.read_resource(AnyUrl("test://image")) 57 | assert len(string_result.contents) == 1 58 | assert ( 59 | getattr(string_result.contents[0], "text") == base64_string 60 | ), "Base64 string mismatch" 61 | assert ( 62 | string_result.contents[0].mimeType == "image/png" 63 | ), "String content mime type not preserved" 64 | 65 | bytes_result = await client.read_resource(AnyUrl("test://image_bytes")) 66 | assert len(bytes_result.contents) == 1 67 | assert ( 68 | base64.b64decode(getattr(bytes_result.contents[0], "blob")) == image_bytes 69 | ), "Bytes mismatch" 70 | assert ( 71 | bytes_result.contents[0].mimeType == "image/png" 72 | ), "Bytes content mime type not preserved" 73 | 74 | 75 | async def test_lowlevel_resource_mime_type(): 76 | """Test that mime_type parameter is respected for resources.""" 77 | server = Server("test") 78 | 79 | # Create a small test image as bytes 80 | image_bytes = b"fake_image_data" 81 | base64_string = base64.b64encode(image_bytes).decode("utf-8") 82 | 83 | # Create test resources with specific mime types 84 | test_resources = [ 85 | types.Resource( 86 | uri=AnyUrl("test://image"), name="test image", mimeType="image/png" 87 | ), 88 | types.Resource( 89 | uri=AnyUrl("test://image_bytes"), 90 | name="test image bytes", 91 | mimeType="image/png", 92 | ), 93 | ] 94 | 95 | @server.list_resources() 96 | async def handle_list_resources(): 97 | return test_resources 98 | 99 | @server.read_resource() 100 | async def handle_read_resource(uri: AnyUrl): 101 | if str(uri) == "test://image": 102 | return [ReadResourceContents(content=base64_string, mime_type="image/png")] 103 | elif str(uri) == "test://image_bytes": 104 | return [ 105 | ReadResourceContents(content=bytes(image_bytes), mime_type="image/png") 106 | ] 107 | raise Exception(f"Resource not found: {uri}") 108 | 109 | # Test that resources are listed with correct mime type 110 | async with client_session(server) as client: 111 | # List resources and verify mime types 112 | resources = await client.list_resources() 113 | assert resources.resources is not None 114 | 115 | mapping = {str(r.uri): r for r in resources.resources} 116 | 117 | # Find our resources 118 | string_resource = mapping["test://image"] 119 | bytes_resource = mapping["test://image_bytes"] 120 | 121 | # Verify mime types 122 | assert ( 123 | string_resource.mimeType == "image/png" 124 | ), "String resource mime type not respected" 125 | assert ( 126 | bytes_resource.mimeType == "image/png" 127 | ), "Bytes resource mime type not respected" 128 | 129 | # Also verify the content can be read correctly 130 | string_result = await client.read_resource(AnyUrl("test://image")) 131 | assert len(string_result.contents) == 1 132 | assert ( 133 | getattr(string_result.contents[0], "text") == base64_string 134 | ), "Base64 string mismatch" 135 | assert ( 136 | string_result.contents[0].mimeType == "image/png" 137 | ), "String content mime type not preserved" 138 | 139 | bytes_result = await client.read_resource(AnyUrl("test://image_bytes")) 140 | assert len(bytes_result.contents) == 1 141 | assert ( 142 | base64.b64decode(getattr(bytes_result.contents[0], "blob")) == image_bytes 143 | ), "Bytes mismatch" 144 | assert ( 145 | bytes_result.contents[0].mimeType == "image/png" 146 | ), "Bytes content mime type not preserved" 147 | -------------------------------------------------------------------------------- /tests/issues/test_176_progress_token.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import AsyncMock, MagicMock 2 | 3 | import pytest 4 | 5 | from mcp.server.fastmcp import Context 6 | from mcp.shared.context import RequestContext 7 | 8 | pytestmark = pytest.mark.anyio 9 | 10 | 11 | async def test_progress_token_zero_first_call(): 12 | """Test that progress notifications work when progress_token is 0 on first call.""" 13 | 14 | # Create mock session with progress notification tracking 15 | mock_session = AsyncMock() 16 | mock_session.send_progress_notification = AsyncMock() 17 | 18 | # Create request context with progress token 0 19 | mock_meta = MagicMock() 20 | mock_meta.progressToken = 0 # This is the key test case - token is 0 21 | 22 | request_context = RequestContext( 23 | request_id="test-request", 24 | session=mock_session, 25 | meta=mock_meta, 26 | lifespan_context=None, 27 | ) 28 | 29 | # Create context with our mocks 30 | ctx = Context(request_context=request_context, fastmcp=MagicMock()) 31 | 32 | # Test progress reporting 33 | await ctx.report_progress(0, 10) # First call with 0 34 | await ctx.report_progress(5, 10) # Middle progress 35 | await ctx.report_progress(10, 10) # Complete 36 | 37 | # Verify progress notifications 38 | assert ( 39 | mock_session.send_progress_notification.call_count == 3 40 | ), "All progress notifications should be sent" 41 | mock_session.send_progress_notification.assert_any_call( 42 | progress_token=0, progress=0.0, total=10.0 43 | ) 44 | mock_session.send_progress_notification.assert_any_call( 45 | progress_token=0, progress=5.0, total=10.0 46 | ) 47 | mock_session.send_progress_notification.assert_any_call( 48 | progress_token=0, progress=10.0, total=10.0 49 | ) 50 | -------------------------------------------------------------------------------- /tests/issues/test_188_concurrency.py: -------------------------------------------------------------------------------- 1 | import anyio 2 | import pytest 3 | from pydantic import AnyUrl 4 | 5 | from mcp.server.fastmcp import FastMCP 6 | from mcp.shared.memory import ( 7 | create_connected_server_and_client_session as create_session, 8 | ) 9 | 10 | _sleep_time_seconds = 0.01 11 | _resource_name = "slow://slow_resource" 12 | 13 | 14 | @pytest.mark.anyio 15 | async def test_messages_are_executed_concurrently(): 16 | server = FastMCP("test") 17 | 18 | @server.tool("sleep") 19 | async def sleep_tool(): 20 | await anyio.sleep(_sleep_time_seconds) 21 | return "done" 22 | 23 | @server.resource(_resource_name) 24 | async def slow_resource(): 25 | await anyio.sleep(_sleep_time_seconds) 26 | return "slow" 27 | 28 | async with create_session(server._mcp_server) as client_session: 29 | start_time = anyio.current_time() 30 | async with anyio.create_task_group() as tg: 31 | for _ in range(10): 32 | tg.start_soon(client_session.call_tool, "sleep") 33 | tg.start_soon(client_session.read_resource, AnyUrl(_resource_name)) 34 | 35 | end_time = anyio.current_time() 36 | 37 | duration = end_time - start_time 38 | assert duration < 3 * _sleep_time_seconds 39 | print(duration) 40 | 41 | 42 | def main(): 43 | anyio.run(test_messages_are_executed_concurrently) 44 | 45 | 46 | if __name__ == "__main__": 47 | import logging 48 | 49 | logging.basicConfig(level=logging.DEBUG) 50 | 51 | main() 52 | -------------------------------------------------------------------------------- /tests/issues/test_192_request_id.py: -------------------------------------------------------------------------------- 1 | import anyio 2 | import pytest 3 | 4 | from mcp.server.lowlevel import NotificationOptions, Server 5 | from mcp.server.models import InitializationOptions 6 | from mcp.types import ( 7 | LATEST_PROTOCOL_VERSION, 8 | ClientCapabilities, 9 | Implementation, 10 | InitializeRequestParams, 11 | JSONRPCMessage, 12 | JSONRPCNotification, 13 | JSONRPCRequest, 14 | NotificationParams, 15 | ) 16 | 17 | 18 | @pytest.mark.anyio 19 | async def test_request_id_match() -> None: 20 | """Test that the server preserves request IDs in responses.""" 21 | server = Server("test") 22 | custom_request_id = "test-123" 23 | 24 | # Create memory streams for communication 25 | client_writer, client_reader = anyio.create_memory_object_stream(1) 26 | server_writer, server_reader = anyio.create_memory_object_stream(1) 27 | 28 | # Server task to process the request 29 | async def run_server(): 30 | async with client_reader, server_writer: 31 | await server.run( 32 | client_reader, 33 | server_writer, 34 | InitializationOptions( 35 | server_name="test", 36 | server_version="1.0.0", 37 | capabilities=server.get_capabilities( 38 | notification_options=NotificationOptions(), 39 | experimental_capabilities={}, 40 | ), 41 | ), 42 | raise_exceptions=True, 43 | ) 44 | 45 | # Start server task 46 | async with ( 47 | anyio.create_task_group() as tg, 48 | client_writer, 49 | client_reader, 50 | server_writer, 51 | server_reader, 52 | ): 53 | tg.start_soon(run_server) 54 | 55 | # Send initialize request 56 | init_req = JSONRPCRequest( 57 | id="init-1", 58 | method="initialize", 59 | params=InitializeRequestParams( 60 | protocolVersion=LATEST_PROTOCOL_VERSION, 61 | capabilities=ClientCapabilities(), 62 | clientInfo=Implementation(name="test-client", version="1.0.0"), 63 | ).model_dump(by_alias=True, exclude_none=True), 64 | jsonrpc="2.0", 65 | ) 66 | 67 | await client_writer.send(JSONRPCMessage(root=init_req)) 68 | await server_reader.receive() # Get init response but don't need to check it 69 | 70 | # Send initialized notification 71 | initialized_notification = JSONRPCNotification( 72 | method="notifications/initialized", 73 | params=NotificationParams().model_dump(by_alias=True, exclude_none=True), 74 | jsonrpc="2.0", 75 | ) 76 | await client_writer.send(JSONRPCMessage(root=initialized_notification)) 77 | 78 | # Send ping request with custom ID 79 | ping_request = JSONRPCRequest( 80 | id=custom_request_id, method="ping", params={}, jsonrpc="2.0" 81 | ) 82 | 83 | await client_writer.send(JSONRPCMessage(root=ping_request)) 84 | 85 | # Read response 86 | response = await server_reader.receive() 87 | 88 | # Verify response ID matches request ID 89 | assert ( 90 | response.root.id == custom_request_id 91 | ), "Response ID should match request ID" 92 | 93 | # Cancel server task 94 | tg.cancel_scope.cancel() 95 | -------------------------------------------------------------------------------- /tests/issues/test_342_base64_encoding.py: -------------------------------------------------------------------------------- 1 | """Test for base64 encoding issue in MCP server. 2 | 3 | This test demonstrates the issue in server.py where the server uses 4 | urlsafe_b64encode but the BlobResourceContents validator expects standard 5 | base64 encoding. 6 | 7 | The test should FAIL before fixing server.py to use b64encode instead of 8 | urlsafe_b64encode. 9 | After the fix, the test should PASS. 10 | """ 11 | 12 | import base64 13 | from typing import cast 14 | 15 | import pytest 16 | from pydantic import AnyUrl 17 | 18 | from mcp.server.lowlevel.helper_types import ReadResourceContents 19 | from mcp.server.lowlevel.server import Server 20 | from mcp.types import ( 21 | BlobResourceContents, 22 | ReadResourceRequest, 23 | ReadResourceRequestParams, 24 | ReadResourceResult, 25 | ServerResult, 26 | ) 27 | 28 | 29 | @pytest.mark.anyio 30 | async def test_server_base64_encoding_issue(): 31 | """Tests that server response can be validated by BlobResourceContents. 32 | 33 | This test will: 34 | 1. Set up a server that returns binary data 35 | 2. Extract the base64-encoded blob from the server's response 36 | 3. Verify the encoded data can be properly validated by BlobResourceContents 37 | 38 | BEFORE FIX: The test will fail because server uses urlsafe_b64encode 39 | AFTER FIX: The test will pass because server uses standard b64encode 40 | """ 41 | server = Server("test") 42 | 43 | # Create binary data that will definitely result in + and / characters 44 | # when encoded with standard base64 45 | binary_data = bytes(list(range(255)) * 4) 46 | 47 | # Register a resource handler that returns our test data 48 | @server.read_resource() 49 | async def read_resource(uri: AnyUrl) -> list[ReadResourceContents]: 50 | return [ 51 | ReadResourceContents( 52 | content=binary_data, mime_type="application/octet-stream" 53 | ) 54 | ] 55 | 56 | # Get the handler directly from the server 57 | handler = server.request_handlers[ReadResourceRequest] 58 | 59 | # Create a request 60 | request = ReadResourceRequest( 61 | method="resources/read", 62 | params=ReadResourceRequestParams(uri=AnyUrl("test://resource")), 63 | ) 64 | 65 | # Call the handler to get the response 66 | result: ServerResult = await handler(request) 67 | 68 | # After (fixed code): 69 | read_result: ReadResourceResult = cast(ReadResourceResult, result.root) 70 | blob_content = read_result.contents[0] 71 | 72 | # First verify our test data actually produces different encodings 73 | urlsafe_b64 = base64.urlsafe_b64encode(binary_data).decode() 74 | standard_b64 = base64.b64encode(binary_data).decode() 75 | assert urlsafe_b64 != standard_b64, "Test data doesn't demonstrate" 76 | " encoding difference" 77 | 78 | # Now validate the server's output with BlobResourceContents.model_validate 79 | # Before the fix: This should fail with "Invalid base64" because server 80 | # uses urlsafe_b64encode 81 | # After the fix: This should pass because server will use standard b64encode 82 | model_dict = blob_content.model_dump() 83 | 84 | # Direct validation - this will fail before fix, pass after fix 85 | blob_model = BlobResourceContents.model_validate(model_dict) 86 | 87 | # Verify we can decode the data back correctly 88 | decoded = base64.b64decode(blob_model.blob) 89 | assert decoded == binary_data 90 | -------------------------------------------------------------------------------- /tests/issues/test_355_type_error.py: -------------------------------------------------------------------------------- 1 | from collections.abc import AsyncIterator 2 | from contextlib import asynccontextmanager 3 | from dataclasses import dataclass 4 | 5 | from mcp.server.fastmcp import Context, FastMCP 6 | 7 | 8 | class Database: # Replace with your actual DB type 9 | @classmethod 10 | async def connect(cls): 11 | return cls() 12 | 13 | async def disconnect(self): 14 | pass 15 | 16 | def query(self): 17 | return "Hello, World!" 18 | 19 | 20 | # Create a named server 21 | mcp = FastMCP("My App") 22 | 23 | 24 | @dataclass 25 | class AppContext: 26 | db: Database 27 | 28 | 29 | @asynccontextmanager 30 | async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]: 31 | """Manage application lifecycle with type-safe context""" 32 | # Initialize on startup 33 | db = await Database.connect() 34 | try: 35 | yield AppContext(db=db) 36 | finally: 37 | # Cleanup on shutdown 38 | await db.disconnect() 39 | 40 | 41 | # Pass lifespan to server 42 | mcp = FastMCP("My App", lifespan=app_lifespan) 43 | 44 | 45 | # Access type-safe lifespan context in tools 46 | @mcp.tool() 47 | def query_db(ctx: Context) -> str: 48 | """Tool that uses initialized resources""" 49 | db = ctx.request_context.lifespan_context.db 50 | return db.query() 51 | -------------------------------------------------------------------------------- /tests/issues/test_88_random_error.py: -------------------------------------------------------------------------------- 1 | """Test to reproduce issue #88: Random error thrown on response.""" 2 | 3 | from collections.abc import Sequence 4 | from datetime import timedelta 5 | from pathlib import Path 6 | 7 | import anyio 8 | import pytest 9 | from anyio.abc import TaskStatus 10 | 11 | from mcp.client.session import ClientSession 12 | from mcp.server.lowlevel import Server 13 | from mcp.shared.exceptions import McpError 14 | from mcp.types import ( 15 | EmbeddedResource, 16 | ImageContent, 17 | TextContent, 18 | ) 19 | 20 | 21 | @pytest.mark.anyio 22 | async def test_notification_validation_error(tmp_path: Path): 23 | """Test that timeouts are handled gracefully and don't break the server. 24 | 25 | This test verifies that when a client request times out: 26 | 1. The server task stays alive 27 | 2. The server can still handle new requests 28 | 3. The client can make new requests 29 | 4. No resources are leaked 30 | """ 31 | 32 | server = Server(name="test") 33 | request_count = 0 34 | slow_request_started = anyio.Event() 35 | slow_request_complete = anyio.Event() 36 | 37 | @server.call_tool() 38 | async def slow_tool( 39 | name: str, arg 40 | ) -> Sequence[TextContent | ImageContent | EmbeddedResource]: 41 | nonlocal request_count 42 | request_count += 1 43 | 44 | if name == "slow": 45 | # Signal that slow request has started 46 | slow_request_started.set() 47 | # Long enough to ensure timeout 48 | await anyio.sleep(0.2) 49 | # Signal completion 50 | slow_request_complete.set() 51 | return [TextContent(type="text", text=f"slow {request_count}")] 52 | elif name == "fast": 53 | # Fast enough to complete before timeout 54 | await anyio.sleep(0.01) 55 | return [TextContent(type="text", text=f"fast {request_count}")] 56 | return [TextContent(type="text", text=f"unknown {request_count}")] 57 | 58 | async def server_handler( 59 | read_stream, 60 | write_stream, 61 | task_status: TaskStatus[str] = anyio.TASK_STATUS_IGNORED, 62 | ): 63 | with anyio.CancelScope() as scope: 64 | task_status.started(scope) # type: ignore 65 | await server.run( 66 | read_stream, 67 | write_stream, 68 | server.create_initialization_options(), 69 | raise_exceptions=True, 70 | ) 71 | 72 | async def client(read_stream, write_stream, scope): 73 | # Use a timeout that's: 74 | # - Long enough for fast operations (>10ms) 75 | # - Short enough for slow operations (<200ms) 76 | # - Not too short to avoid flakiness 77 | async with ClientSession( 78 | read_stream, write_stream, read_timeout_seconds=timedelta(milliseconds=50) 79 | ) as session: 80 | await session.initialize() 81 | 82 | # First call should work (fast operation) 83 | result = await session.call_tool("fast") 84 | assert result.content == [TextContent(type="text", text="fast 1")] 85 | assert not slow_request_complete.is_set() 86 | 87 | # Second call should timeout (slow operation) 88 | with pytest.raises(McpError) as exc_info: 89 | await session.call_tool("slow") 90 | assert "Timed out while waiting" in str(exc_info.value) 91 | 92 | # Wait for slow request to complete in the background 93 | with anyio.fail_after(1): # Timeout after 1 second 94 | await slow_request_complete.wait() 95 | 96 | # Third call should work (fast operation), 97 | # proving server is still responsive 98 | result = await session.call_tool("fast") 99 | assert result.content == [TextContent(type="text", text="fast 3")] 100 | scope.cancel() 101 | 102 | # Run server and client in separate task groups to avoid cancellation 103 | server_writer, server_reader = anyio.create_memory_object_stream(1) 104 | client_writer, client_reader = anyio.create_memory_object_stream(1) 105 | 106 | async with anyio.create_task_group() as tg: 107 | scope = await tg.start(server_handler, server_reader, client_writer) 108 | # Run client in a separate task to avoid cancellation 109 | tg.start_soon(client, client_reader, server_writer, scope) 110 | -------------------------------------------------------------------------------- /tests/server/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/b4c7db6a50a5c88bae1db5c1f7fba44d16eebc6e/tests/server/__init__.py -------------------------------------------------------------------------------- /tests/server/fastmcp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/b4c7db6a50a5c88bae1db5c1f7fba44d16eebc6e/tests/server/fastmcp/__init__.py -------------------------------------------------------------------------------- /tests/server/fastmcp/prompts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/b4c7db6a50a5c88bae1db5c1f7fba44d16eebc6e/tests/server/fastmcp/prompts/__init__.py -------------------------------------------------------------------------------- /tests/server/fastmcp/prompts/test_manager.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from mcp.server.fastmcp.prompts.base import Prompt, TextContent, UserMessage 4 | from mcp.server.fastmcp.prompts.manager import PromptManager 5 | 6 | 7 | class TestPromptManager: 8 | def test_add_prompt(self): 9 | """Test adding a prompt to the manager.""" 10 | 11 | def fn() -> str: 12 | return "Hello, world!" 13 | 14 | manager = PromptManager() 15 | prompt = Prompt.from_function(fn) 16 | added = manager.add_prompt(prompt) 17 | assert added == prompt 18 | assert manager.get_prompt("fn") == prompt 19 | 20 | def test_add_duplicate_prompt(self, caplog): 21 | """Test adding the same prompt twice.""" 22 | 23 | def fn() -> str: 24 | return "Hello, world!" 25 | 26 | manager = PromptManager() 27 | prompt = Prompt.from_function(fn) 28 | first = manager.add_prompt(prompt) 29 | second = manager.add_prompt(prompt) 30 | assert first == second 31 | assert "Prompt already exists" in caplog.text 32 | 33 | def test_disable_warn_on_duplicate_prompts(self, caplog): 34 | """Test disabling warning on duplicate prompts.""" 35 | 36 | def fn() -> str: 37 | return "Hello, world!" 38 | 39 | manager = PromptManager(warn_on_duplicate_prompts=False) 40 | prompt = Prompt.from_function(fn) 41 | first = manager.add_prompt(prompt) 42 | second = manager.add_prompt(prompt) 43 | assert first == second 44 | assert "Prompt already exists" not in caplog.text 45 | 46 | def test_list_prompts(self): 47 | """Test listing all prompts.""" 48 | 49 | def fn1() -> str: 50 | return "Hello, world!" 51 | 52 | def fn2() -> str: 53 | return "Goodbye, world!" 54 | 55 | manager = PromptManager() 56 | prompt1 = Prompt.from_function(fn1) 57 | prompt2 = Prompt.from_function(fn2) 58 | manager.add_prompt(prompt1) 59 | manager.add_prompt(prompt2) 60 | prompts = manager.list_prompts() 61 | assert len(prompts) == 2 62 | assert prompts == [prompt1, prompt2] 63 | 64 | @pytest.mark.anyio 65 | async def test_render_prompt(self): 66 | """Test rendering a prompt.""" 67 | 68 | def fn() -> str: 69 | return "Hello, world!" 70 | 71 | manager = PromptManager() 72 | prompt = Prompt.from_function(fn) 73 | manager.add_prompt(prompt) 74 | messages = await manager.render_prompt("fn") 75 | assert messages == [ 76 | UserMessage(content=TextContent(type="text", text="Hello, world!")) 77 | ] 78 | 79 | @pytest.mark.anyio 80 | async def test_render_prompt_with_args(self): 81 | """Test rendering a prompt with arguments.""" 82 | 83 | def fn(name: str) -> str: 84 | return f"Hello, {name}!" 85 | 86 | manager = PromptManager() 87 | prompt = Prompt.from_function(fn) 88 | manager.add_prompt(prompt) 89 | messages = await manager.render_prompt("fn", arguments={"name": "World"}) 90 | assert messages == [ 91 | UserMessage(content=TextContent(type="text", text="Hello, World!")) 92 | ] 93 | 94 | @pytest.mark.anyio 95 | async def test_render_unknown_prompt(self): 96 | """Test rendering a non-existent prompt.""" 97 | manager = PromptManager() 98 | with pytest.raises(ValueError, match="Unknown prompt: unknown"): 99 | await manager.render_prompt("unknown") 100 | 101 | @pytest.mark.anyio 102 | async def test_render_prompt_with_missing_args(self): 103 | """Test rendering a prompt with missing required arguments.""" 104 | 105 | def fn(name: str) -> str: 106 | return f"Hello, {name}!" 107 | 108 | manager = PromptManager() 109 | prompt = Prompt.from_function(fn) 110 | manager.add_prompt(prompt) 111 | with pytest.raises(ValueError, match="Missing required arguments"): 112 | await manager.render_prompt("fn") 113 | -------------------------------------------------------------------------------- /tests/server/fastmcp/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/b4c7db6a50a5c88bae1db5c1f7fba44d16eebc6e/tests/server/fastmcp/resources/__init__.py -------------------------------------------------------------------------------- /tests/server/fastmcp/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 mcp.server.fastmcp.resources import FileResource 9 | 10 | 11 | @pytest.fixture 12 | def temp_file(): 13 | """Create a temporary file for testing. 14 | 15 | File is automatically cleaned up after the test if it still exists. 16 | """ 17 | content = "test content" 18 | with NamedTemporaryFile(mode="w", delete=False) as f: 19 | f.write(content) 20 | path = Path(f.name).resolve() 21 | yield path 22 | try: 23 | path.unlink() 24 | except FileNotFoundError: 25 | pass # File was already deleted by the test 26 | 27 | 28 | class TestFileResource: 29 | """Test FileResource functionality.""" 30 | 31 | def test_file_resource_creation(self, temp_file: Path): 32 | """Test creating a FileResource.""" 33 | resource = FileResource( 34 | uri=FileUrl(temp_file.as_uri()), 35 | name="test", 36 | description="test file", 37 | path=temp_file, 38 | ) 39 | assert str(resource.uri) == temp_file.as_uri() 40 | assert resource.name == "test" 41 | assert resource.description == "test file" 42 | assert resource.mime_type == "text/plain" # default 43 | assert resource.path == temp_file 44 | assert resource.is_binary is False # default 45 | 46 | def test_file_resource_str_path_conversion(self, temp_file: Path): 47 | """Test FileResource handles string paths.""" 48 | resource = FileResource( 49 | uri=FileUrl(f"file://{temp_file}"), 50 | name="test", 51 | path=Path(str(temp_file)), 52 | ) 53 | assert isinstance(resource.path, Path) 54 | assert resource.path.is_absolute() 55 | 56 | @pytest.mark.anyio 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 | @pytest.mark.anyio 69 | async def test_read_binary_file(self, temp_file: Path): 70 | """Test reading a file as binary.""" 71 | resource = FileResource( 72 | uri=FileUrl(f"file://{temp_file}"), 73 | name="test", 74 | path=temp_file, 75 | is_binary=True, 76 | ) 77 | content = await resource.read() 78 | assert isinstance(content, bytes) 79 | assert content == b"test content" 80 | 81 | def test_relative_path_error(self): 82 | """Test error on relative path.""" 83 | with pytest.raises(ValueError, match="Path must be absolute"): 84 | FileResource( 85 | uri=FileUrl("file:///test.txt"), 86 | name="test", 87 | path=Path("test.txt"), 88 | ) 89 | 90 | @pytest.mark.anyio 91 | async def test_missing_file_error(self, temp_file: Path): 92 | """Test error when file doesn't exist.""" 93 | # Create path to non-existent file 94 | missing = temp_file.parent / "missing.txt" 95 | resource = FileResource( 96 | uri=FileUrl("file:///missing.txt"), 97 | name="test", 98 | path=missing, 99 | ) 100 | with pytest.raises(ValueError, match="Error reading file"): 101 | await resource.read() 102 | 103 | @pytest.mark.skipif( 104 | os.name == "nt", reason="File permissions behave differently on Windows" 105 | ) 106 | @pytest.mark.anyio 107 | async def test_permission_error(self, temp_file: Path): 108 | """Test reading a file without permissions.""" 109 | temp_file.chmod(0o000) # Remove all permissions 110 | try: 111 | resource = FileResource( 112 | uri=FileUrl(temp_file.as_uri()), 113 | name="test", 114 | path=temp_file, 115 | ) 116 | with pytest.raises(ValueError, match="Error reading file"): 117 | await resource.read() 118 | finally: 119 | temp_file.chmod(0o644) # Restore permissions 120 | -------------------------------------------------------------------------------- /tests/server/fastmcp/resources/test_function_resources.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pydantic import AnyUrl, BaseModel 3 | 4 | from mcp.server.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 | @pytest.mark.anyio 29 | async def test_read_text(self): 30 | """Test reading text from a FunctionResource.""" 31 | 32 | def get_data() -> str: 33 | return "Hello, world!" 34 | 35 | resource = FunctionResource( 36 | uri=AnyUrl("function://test"), 37 | name="test", 38 | fn=get_data, 39 | ) 40 | content = await resource.read() 41 | assert content == "Hello, world!" 42 | assert resource.mime_type == "text/plain" 43 | 44 | @pytest.mark.anyio 45 | async def test_read_binary(self): 46 | """Test reading binary data from a FunctionResource.""" 47 | 48 | def get_data() -> bytes: 49 | return b"Hello, world!" 50 | 51 | resource = FunctionResource( 52 | uri=AnyUrl("function://test"), 53 | name="test", 54 | fn=get_data, 55 | ) 56 | content = await resource.read() 57 | assert content == b"Hello, world!" 58 | 59 | @pytest.mark.anyio 60 | async def test_json_conversion(self): 61 | """Test automatic JSON conversion of non-string results.""" 62 | 63 | def get_data() -> dict: 64 | return {"key": "value"} 65 | 66 | resource = FunctionResource( 67 | uri=AnyUrl("function://test"), 68 | name="test", 69 | fn=get_data, 70 | ) 71 | content = await resource.read() 72 | assert isinstance(content, str) 73 | assert '"key": "value"' in content 74 | 75 | @pytest.mark.anyio 76 | async def test_error_handling(self): 77 | """Test error handling in FunctionResource.""" 78 | 79 | def failing_func() -> str: 80 | raise ValueError("Test error") 81 | 82 | resource = FunctionResource( 83 | uri=AnyUrl("function://test"), 84 | name="test", 85 | fn=failing_func, 86 | ) 87 | with pytest.raises(ValueError, match="Error reading resource function://test"): 88 | await resource.read() 89 | 90 | @pytest.mark.anyio 91 | async def test_basemodel_conversion(self): 92 | """Test handling of BaseModel types.""" 93 | 94 | class MyModel(BaseModel): 95 | name: str 96 | 97 | resource = FunctionResource( 98 | uri=AnyUrl("function://test"), 99 | name="test", 100 | fn=lambda: MyModel(name="test"), 101 | ) 102 | content = await resource.read() 103 | assert content == '{"name": "test"}' 104 | 105 | @pytest.mark.anyio 106 | async def test_custom_type_conversion(self): 107 | """Test handling of custom types.""" 108 | 109 | class CustomData: 110 | def __str__(self) -> str: 111 | return "custom data" 112 | 113 | def get_data() -> CustomData: 114 | return CustomData() 115 | 116 | resource = FunctionResource( 117 | uri=AnyUrl("function://test"), 118 | name="test", 119 | fn=get_data, 120 | ) 121 | content = await resource.read() 122 | assert isinstance(content, str) 123 | 124 | @pytest.mark.anyio 125 | async def test_async_read_text(self): 126 | """Test reading text from async FunctionResource.""" 127 | 128 | async def get_data() -> str: 129 | return "Hello, world!" 130 | 131 | resource = FunctionResource( 132 | uri=AnyUrl("function://test"), 133 | name="test", 134 | fn=get_data, 135 | ) 136 | content = await resource.read() 137 | assert content == "Hello, world!" 138 | assert resource.mime_type == "text/plain" 139 | -------------------------------------------------------------------------------- /tests/server/fastmcp/resources/test_resource_manager.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from tempfile import NamedTemporaryFile 3 | 4 | import pytest 5 | from pydantic import AnyUrl, FileUrl 6 | 7 | from mcp.server.fastmcp.resources import ( 8 | FileResource, 9 | FunctionResource, 10 | ResourceManager, 11 | ResourceTemplate, 12 | ) 13 | 14 | 15 | @pytest.fixture 16 | def temp_file(): 17 | """Create a temporary file for testing. 18 | 19 | File is automatically cleaned up after the test if it still exists. 20 | """ 21 | content = "test content" 22 | with NamedTemporaryFile(mode="w", delete=False) as f: 23 | f.write(content) 24 | path = Path(f.name).resolve() 25 | yield path 26 | try: 27 | path.unlink() 28 | except FileNotFoundError: 29 | pass # File was already deleted by the test 30 | 31 | 32 | class TestResourceManager: 33 | """Test ResourceManager functionality.""" 34 | 35 | def test_add_resource(self, temp_file: Path): 36 | """Test adding a resource.""" 37 | manager = ResourceManager() 38 | resource = FileResource( 39 | uri=FileUrl(f"file://{temp_file}"), 40 | name="test", 41 | path=temp_file, 42 | ) 43 | added = manager.add_resource(resource) 44 | assert added == resource 45 | assert manager.list_resources() == [resource] 46 | 47 | def test_add_duplicate_resource(self, temp_file: Path): 48 | """Test adding the same resource twice.""" 49 | manager = ResourceManager() 50 | resource = FileResource( 51 | uri=FileUrl(f"file://{temp_file}"), 52 | name="test", 53 | path=temp_file, 54 | ) 55 | first = manager.add_resource(resource) 56 | second = manager.add_resource(resource) 57 | assert first == second 58 | assert manager.list_resources() == [resource] 59 | 60 | def test_warn_on_duplicate_resources(self, temp_file: Path, caplog): 61 | """Test warning on duplicate resources.""" 62 | manager = ResourceManager() 63 | resource = FileResource( 64 | uri=FileUrl(f"file://{temp_file}"), 65 | name="test", 66 | path=temp_file, 67 | ) 68 | manager.add_resource(resource) 69 | manager.add_resource(resource) 70 | assert "Resource already exists" in caplog.text 71 | 72 | def test_disable_warn_on_duplicate_resources(self, temp_file: Path, caplog): 73 | """Test disabling warning on duplicate resources.""" 74 | manager = ResourceManager(warn_on_duplicate_resources=False) 75 | resource = FileResource( 76 | uri=FileUrl(f"file://{temp_file}"), 77 | name="test", 78 | path=temp_file, 79 | ) 80 | manager.add_resource(resource) 81 | manager.add_resource(resource) 82 | assert "Resource already exists" not in caplog.text 83 | 84 | @pytest.mark.anyio 85 | async def test_get_resource(self, temp_file: Path): 86 | """Test getting a resource by URI.""" 87 | manager = ResourceManager() 88 | resource = FileResource( 89 | uri=FileUrl(f"file://{temp_file}"), 90 | name="test", 91 | path=temp_file, 92 | ) 93 | manager.add_resource(resource) 94 | retrieved = await manager.get_resource(resource.uri) 95 | assert retrieved == resource 96 | 97 | @pytest.mark.anyio 98 | async def test_get_resource_from_template(self): 99 | """Test getting a resource through a template.""" 100 | manager = ResourceManager() 101 | 102 | def greet(name: str) -> str: 103 | return f"Hello, {name}!" 104 | 105 | template = ResourceTemplate.from_function( 106 | fn=greet, 107 | uri_template="greet://{name}", 108 | name="greeter", 109 | ) 110 | manager._templates[template.uri_template] = template 111 | 112 | resource = await manager.get_resource(AnyUrl("greet://world")) 113 | assert isinstance(resource, FunctionResource) 114 | content = await resource.read() 115 | assert content == "Hello, world!" 116 | 117 | @pytest.mark.anyio 118 | async def test_get_unknown_resource(self): 119 | """Test getting a non-existent resource.""" 120 | manager = ResourceManager() 121 | with pytest.raises(ValueError, match="Unknown resource"): 122 | await manager.get_resource(AnyUrl("unknown://test")) 123 | 124 | def test_list_resources(self, temp_file: Path): 125 | """Test listing all resources.""" 126 | manager = ResourceManager() 127 | resource1 = FileResource( 128 | uri=FileUrl(f"file://{temp_file}"), 129 | name="test1", 130 | path=temp_file, 131 | ) 132 | resource2 = FileResource( 133 | uri=FileUrl(f"file://{temp_file}2"), 134 | name="test2", 135 | path=temp_file, 136 | ) 137 | manager.add_resource(resource1) 138 | manager.add_resource(resource2) 139 | resources = manager.list_resources() 140 | assert len(resources) == 2 141 | assert resources == [resource1, resource2] 142 | -------------------------------------------------------------------------------- /tests/server/fastmcp/resources/test_resources.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pydantic import AnyUrl 3 | 4 | from mcp.server.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 | @pytest.mark.anyio 94 | async def test_resource_read_abstract(self): 95 | """Test that Resource.read() is abstract.""" 96 | 97 | class ConcreteResource(Resource): 98 | pass 99 | 100 | with pytest.raises(TypeError, match="abstract method"): 101 | ConcreteResource(uri=AnyUrl("test://test"), name="test") # type: ignore 102 | -------------------------------------------------------------------------------- /tests/server/fastmcp/servers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/b4c7db6a50a5c88bae1db5c1f7fba44d16eebc6e/tests/server/fastmcp/servers/__init__.py -------------------------------------------------------------------------------- /tests/server/fastmcp/servers/test_file_server.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | import pytest 5 | 6 | from mcp.server.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 | @pytest.mark.anyio 77 | async def test_list_resources(mcp: FastMCP): 78 | resources = await mcp.list_resources() 79 | assert len(resources) == 4 80 | 81 | assert [str(r.uri) for r in resources] == [ 82 | "dir://test_dir", 83 | "file://test_dir/example.py", 84 | "file://test_dir/readme.md", 85 | "file://test_dir/config.json", 86 | ] 87 | 88 | 89 | @pytest.mark.anyio 90 | async def test_read_resource_dir(mcp: FastMCP): 91 | res_iter = await mcp.read_resource("dir://test_dir") 92 | res_list = list(res_iter) 93 | assert len(res_list) == 1 94 | res = res_list[0] 95 | assert res.mime_type == "text/plain" 96 | 97 | files = json.loads(res.content) 98 | 99 | assert sorted([Path(f).name for f in files]) == [ 100 | "config.json", 101 | "example.py", 102 | "readme.md", 103 | ] 104 | 105 | 106 | @pytest.mark.anyio 107 | async def test_read_resource_file(mcp: FastMCP): 108 | res_iter = await mcp.read_resource("file://test_dir/example.py") 109 | res_list = list(res_iter) 110 | assert len(res_list) == 1 111 | res = res_list[0] 112 | assert res.content == "print('hello world')" 113 | 114 | 115 | @pytest.mark.anyio 116 | async def test_delete_file(mcp: FastMCP, test_dir: Path): 117 | await mcp.call_tool("delete_file", arguments={"path": str(test_dir / "example.py")}) 118 | assert not (test_dir / "example.py").exists() 119 | 120 | 121 | @pytest.mark.anyio 122 | async def test_delete_file_and_check_resources(mcp: FastMCP, test_dir: Path): 123 | await mcp.call_tool("delete_file", arguments={"path": str(test_dir / "example.py")}) 124 | res_iter = await mcp.read_resource("file://test_dir/example.py") 125 | res_list = list(res_iter) 126 | assert len(res_list) == 1 127 | res = res_list[0] 128 | assert res.content == "File not found" 129 | -------------------------------------------------------------------------------- /tests/server/fastmcp/test_parameter_descriptions.py: -------------------------------------------------------------------------------- 1 | """Test that parameter descriptions are properly exposed through list_tools""" 2 | 3 | import pytest 4 | from pydantic import Field 5 | 6 | from mcp.server.fastmcp import FastMCP 7 | 8 | 9 | @pytest.mark.anyio 10 | async def test_parameter_descriptions(): 11 | mcp = FastMCP("Test Server") 12 | 13 | @mcp.tool() 14 | def greet( 15 | name: str = Field(description="The name to greet"), 16 | title: str = Field(description="Optional title", default=""), 17 | ) -> str: 18 | """A greeting tool""" 19 | return f"Hello {title} {name}" 20 | 21 | tools = await mcp.list_tools() 22 | assert len(tools) == 1 23 | tool = tools[0] 24 | 25 | # Check that parameter descriptions are present in the schema 26 | properties = tool.inputSchema["properties"] 27 | assert "name" in properties 28 | assert properties["name"]["description"] == "The name to greet" 29 | assert "title" in properties 30 | assert properties["title"]["description"] == "Optional title" 31 | -------------------------------------------------------------------------------- /tests/server/test_read_resource.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Iterable 2 | from pathlib import Path 3 | from tempfile import NamedTemporaryFile 4 | 5 | import pytest 6 | from pydantic import AnyUrl, FileUrl 7 | 8 | import mcp.types as types 9 | from mcp.server.lowlevel.server import ReadResourceContents, Server 10 | 11 | 12 | @pytest.fixture 13 | def temp_file(): 14 | """Create a temporary file for testing.""" 15 | with NamedTemporaryFile(mode="w", delete=False) as f: 16 | f.write("test content") 17 | path = Path(f.name).resolve() 18 | yield path 19 | try: 20 | path.unlink() 21 | except FileNotFoundError: 22 | pass 23 | 24 | 25 | @pytest.mark.anyio 26 | async def test_read_resource_text(temp_file: Path): 27 | server = Server("test") 28 | 29 | @server.read_resource() 30 | async def read_resource(uri: AnyUrl) -> Iterable[ReadResourceContents]: 31 | return [ReadResourceContents(content="Hello World", mime_type="text/plain")] 32 | 33 | # Get the handler directly from the server 34 | handler = server.request_handlers[types.ReadResourceRequest] 35 | 36 | # Create a request 37 | request = types.ReadResourceRequest( 38 | method="resources/read", 39 | params=types.ReadResourceRequestParams(uri=FileUrl(temp_file.as_uri())), 40 | ) 41 | 42 | # Call the handler 43 | result = await handler(request) 44 | assert isinstance(result.root, types.ReadResourceResult) 45 | assert len(result.root.contents) == 1 46 | 47 | content = result.root.contents[0] 48 | assert isinstance(content, types.TextResourceContents) 49 | assert content.text == "Hello World" 50 | assert content.mimeType == "text/plain" 51 | 52 | 53 | @pytest.mark.anyio 54 | async def test_read_resource_binary(temp_file: Path): 55 | server = Server("test") 56 | 57 | @server.read_resource() 58 | async def read_resource(uri: AnyUrl) -> Iterable[ReadResourceContents]: 59 | return [ 60 | ReadResourceContents( 61 | content=b"Hello World", mime_type="application/octet-stream" 62 | ) 63 | ] 64 | 65 | # Get the handler directly from the server 66 | handler = server.request_handlers[types.ReadResourceRequest] 67 | 68 | # Create a request 69 | request = types.ReadResourceRequest( 70 | method="resources/read", 71 | params=types.ReadResourceRequestParams(uri=FileUrl(temp_file.as_uri())), 72 | ) 73 | 74 | # Call the handler 75 | result = await handler(request) 76 | assert isinstance(result.root, types.ReadResourceResult) 77 | assert len(result.root.contents) == 1 78 | 79 | content = result.root.contents[0] 80 | assert isinstance(content, types.BlobResourceContents) 81 | assert content.mimeType == "application/octet-stream" 82 | 83 | 84 | @pytest.mark.anyio 85 | async def test_read_resource_default_mime(temp_file: Path): 86 | server = Server("test") 87 | 88 | @server.read_resource() 89 | async def read_resource(uri: AnyUrl) -> Iterable[ReadResourceContents]: 90 | return [ 91 | ReadResourceContents( 92 | content="Hello World", 93 | # No mime_type specified, should default to text/plain 94 | ) 95 | ] 96 | 97 | # Get the handler directly from the server 98 | handler = server.request_handlers[types.ReadResourceRequest] 99 | 100 | # Create a request 101 | request = types.ReadResourceRequest( 102 | method="resources/read", 103 | params=types.ReadResourceRequestParams(uri=FileUrl(temp_file.as_uri())), 104 | ) 105 | 106 | # Call the handler 107 | result = await handler(request) 108 | assert isinstance(result.root, types.ReadResourceResult) 109 | assert len(result.root.contents) == 1 110 | 111 | content = result.root.contents[0] 112 | assert isinstance(content, types.TextResourceContents) 113 | assert content.text == "Hello World" 114 | assert content.mimeType == "text/plain" 115 | -------------------------------------------------------------------------------- /tests/server/test_session.py: -------------------------------------------------------------------------------- 1 | import anyio 2 | import pytest 3 | 4 | import mcp.types as types 5 | from mcp.client.session import ClientSession 6 | from mcp.server import Server 7 | from mcp.server.lowlevel import NotificationOptions 8 | from mcp.server.models import InitializationOptions 9 | from mcp.server.session import ServerSession 10 | from mcp.shared.session import RequestResponder 11 | from mcp.types import ( 12 | ClientNotification, 13 | InitializedNotification, 14 | JSONRPCMessage, 15 | PromptsCapability, 16 | ResourcesCapability, 17 | ServerCapabilities, 18 | ) 19 | 20 | 21 | @pytest.mark.anyio 22 | async def test_server_session_initialize(): 23 | server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[ 24 | JSONRPCMessage 25 | ](1) 26 | client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[ 27 | JSONRPCMessage 28 | ](1) 29 | 30 | # Create a message handler to catch exceptions 31 | async def message_handler( 32 | message: RequestResponder[types.ServerRequest, types.ClientResult] 33 | | types.ServerNotification 34 | | Exception, 35 | ) -> None: 36 | if isinstance(message, Exception): 37 | raise message 38 | 39 | received_initialized = False 40 | 41 | async def run_server(): 42 | nonlocal received_initialized 43 | 44 | async with ServerSession( 45 | client_to_server_receive, 46 | server_to_client_send, 47 | InitializationOptions( 48 | server_name="mcp", 49 | server_version="0.1.0", 50 | capabilities=ServerCapabilities(), 51 | ), 52 | ) as server_session: 53 | async for message in server_session.incoming_messages: 54 | if isinstance(message, Exception): 55 | raise message 56 | 57 | if isinstance(message, ClientNotification) and isinstance( 58 | message.root, InitializedNotification 59 | ): 60 | received_initialized = True 61 | return 62 | 63 | try: 64 | async with ( 65 | ClientSession( 66 | server_to_client_receive, 67 | client_to_server_send, 68 | message_handler=message_handler, 69 | ) as client_session, 70 | anyio.create_task_group() as tg, 71 | ): 72 | tg.start_soon(run_server) 73 | 74 | await client_session.initialize() 75 | except anyio.ClosedResourceError: 76 | pass 77 | 78 | assert received_initialized 79 | 80 | 81 | @pytest.mark.anyio 82 | async def test_server_capabilities(): 83 | server = Server("test") 84 | notification_options = NotificationOptions() 85 | experimental_capabilities = {} 86 | 87 | # Initially no capabilities 88 | caps = server.get_capabilities(notification_options, experimental_capabilities) 89 | assert caps.prompts is None 90 | assert caps.resources is None 91 | 92 | # Add a prompts handler 93 | @server.list_prompts() 94 | async def list_prompts(): 95 | return [] 96 | 97 | caps = server.get_capabilities(notification_options, experimental_capabilities) 98 | assert caps.prompts == PromptsCapability(listChanged=False) 99 | assert caps.resources is None 100 | 101 | # Add a resources handler 102 | @server.list_resources() 103 | async def list_resources(): 104 | return [] 105 | 106 | caps = server.get_capabilities(notification_options, experimental_capabilities) 107 | assert caps.prompts == PromptsCapability(listChanged=False) 108 | assert caps.resources == ResourcesCapability(subscribe=False, listChanged=False) 109 | -------------------------------------------------------------------------------- /tests/server/test_stdio.py: -------------------------------------------------------------------------------- 1 | import io 2 | 3 | import anyio 4 | import pytest 5 | 6 | from mcp.server.stdio import stdio_server 7 | from mcp.types import JSONRPCMessage, JSONRPCRequest, JSONRPCResponse 8 | 9 | 10 | @pytest.mark.anyio 11 | async def test_stdio_server(): 12 | stdin = io.StringIO() 13 | stdout = io.StringIO() 14 | 15 | messages = [ 16 | JSONRPCMessage(root=JSONRPCRequest(jsonrpc="2.0", id=1, method="ping")), 17 | JSONRPCMessage(root=JSONRPCResponse(jsonrpc="2.0", id=2, result={})), 18 | ] 19 | 20 | for message in messages: 21 | stdin.write(message.model_dump_json(by_alias=True, exclude_none=True) + "\n") 22 | stdin.seek(0) 23 | 24 | async with stdio_server( 25 | stdin=anyio.AsyncFile(stdin), stdout=anyio.AsyncFile(stdout) 26 | ) as (read_stream, write_stream): 27 | received_messages = [] 28 | async with read_stream: 29 | async for message in read_stream: 30 | if isinstance(message, Exception): 31 | raise message 32 | received_messages.append(message) 33 | if len(received_messages) == 2: 34 | break 35 | 36 | # Verify received messages 37 | assert len(received_messages) == 2 38 | assert received_messages[0] == JSONRPCMessage( 39 | root=JSONRPCRequest(jsonrpc="2.0", id=1, method="ping") 40 | ) 41 | assert received_messages[1] == JSONRPCMessage( 42 | root=JSONRPCResponse(jsonrpc="2.0", id=2, result={}) 43 | ) 44 | 45 | # Test sending responses from the server 46 | responses = [ 47 | JSONRPCMessage(root=JSONRPCRequest(jsonrpc="2.0", id=3, method="ping")), 48 | JSONRPCMessage(root=JSONRPCResponse(jsonrpc="2.0", id=4, result={})), 49 | ] 50 | 51 | async with write_stream: 52 | for response in responses: 53 | await write_stream.send(response) 54 | 55 | stdout.seek(0) 56 | output_lines = stdout.readlines() 57 | assert len(output_lines) == 2 58 | 59 | received_responses = [ 60 | JSONRPCMessage.model_validate_json(line.strip()) for line in output_lines 61 | ] 62 | assert len(received_responses) == 2 63 | assert received_responses[0] == JSONRPCMessage( 64 | root=JSONRPCRequest(jsonrpc="2.0", id=3, method="ping") 65 | ) 66 | assert received_responses[1] == JSONRPCMessage( 67 | root=JSONRPCResponse(jsonrpc="2.0", id=4, result={}) 68 | ) 69 | -------------------------------------------------------------------------------- /tests/shared/test_memory.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pydantic import AnyUrl 3 | from typing_extensions import AsyncGenerator 4 | 5 | from mcp.client.session import ClientSession 6 | from mcp.server import Server 7 | from mcp.shared.memory import ( 8 | create_connected_server_and_client_session, 9 | ) 10 | from mcp.types import ( 11 | EmptyResult, 12 | Resource, 13 | ) 14 | 15 | 16 | @pytest.fixture 17 | def mcp_server() -> Server: 18 | server = Server(name="test_server") 19 | 20 | @server.list_resources() 21 | async def handle_list_resources(): 22 | return [ 23 | Resource( 24 | uri=AnyUrl("memory://test"), 25 | name="Test Resource", 26 | description="A test resource", 27 | ) 28 | ] 29 | 30 | return server 31 | 32 | 33 | @pytest.fixture 34 | async def client_connected_to_server( 35 | mcp_server: Server, 36 | ) -> AsyncGenerator[ClientSession, None]: 37 | async with create_connected_server_and_client_session(mcp_server) as client_session: 38 | yield client_session 39 | 40 | 41 | @pytest.mark.anyio 42 | async def test_memory_server_and_client_connection( 43 | client_connected_to_server: ClientSession, 44 | ): 45 | """Shows how a client and server can communicate over memory streams.""" 46 | response = await client_connected_to_server.send_ping() 47 | assert isinstance(response, EmptyResult) 48 | -------------------------------------------------------------------------------- /tests/shared/test_session.py: -------------------------------------------------------------------------------- 1 | from collections.abc import AsyncGenerator 2 | 3 | import anyio 4 | import pytest 5 | 6 | import mcp.types as types 7 | from mcp.client.session import ClientSession 8 | from mcp.server.lowlevel.server import Server 9 | from mcp.shared.exceptions import McpError 10 | from mcp.shared.memory import create_connected_server_and_client_session 11 | from mcp.types import ( 12 | CancelledNotification, 13 | CancelledNotificationParams, 14 | ClientNotification, 15 | ClientRequest, 16 | EmptyResult, 17 | ) 18 | 19 | 20 | @pytest.fixture 21 | def mcp_server() -> Server: 22 | return Server(name="test server") 23 | 24 | 25 | @pytest.fixture 26 | async def client_connected_to_server( 27 | mcp_server: Server, 28 | ) -> AsyncGenerator[ClientSession, None]: 29 | async with create_connected_server_and_client_session(mcp_server) as client_session: 30 | yield client_session 31 | 32 | 33 | @pytest.mark.anyio 34 | async def test_in_flight_requests_cleared_after_completion( 35 | client_connected_to_server: ClientSession, 36 | ): 37 | """Verify that _in_flight is empty after all requests complete.""" 38 | # Send a request and wait for response 39 | response = await client_connected_to_server.send_ping() 40 | assert isinstance(response, EmptyResult) 41 | 42 | # Verify _in_flight is empty 43 | assert len(client_connected_to_server._in_flight) == 0 44 | 45 | 46 | @pytest.mark.anyio 47 | async def test_request_cancellation(): 48 | """Test that requests can be cancelled while in-flight.""" 49 | # The tool is already registered in the fixture 50 | 51 | ev_tool_called = anyio.Event() 52 | ev_cancelled = anyio.Event() 53 | request_id = None 54 | 55 | # Start the request in a separate task so we can cancel it 56 | def make_server() -> Server: 57 | server = Server(name="TestSessionServer") 58 | 59 | # Register the tool handler 60 | @server.call_tool() 61 | async def handle_call_tool(name: str, arguments: dict | None) -> list: 62 | nonlocal request_id, ev_tool_called 63 | if name == "slow_tool": 64 | request_id = server.request_context.request_id 65 | ev_tool_called.set() 66 | await anyio.sleep(10) # Long enough to ensure we can cancel 67 | return [] 68 | raise ValueError(f"Unknown tool: {name}") 69 | 70 | # Register the tool so it shows up in list_tools 71 | @server.list_tools() 72 | async def handle_list_tools() -> list[types.Tool]: 73 | return [ 74 | types.Tool( 75 | name="slow_tool", 76 | description="A slow tool that takes 10 seconds to complete", 77 | inputSchema={}, 78 | ) 79 | ] 80 | 81 | return server 82 | 83 | async def make_request(client_session): 84 | nonlocal ev_cancelled 85 | try: 86 | await client_session.send_request( 87 | ClientRequest( 88 | types.CallToolRequest( 89 | method="tools/call", 90 | params=types.CallToolRequestParams( 91 | name="slow_tool", arguments={} 92 | ), 93 | ) 94 | ), 95 | types.CallToolResult, 96 | ) 97 | pytest.fail("Request should have been cancelled") 98 | except McpError as e: 99 | # Expected - request was cancelled 100 | assert "Request cancelled" in str(e) 101 | ev_cancelled.set() 102 | 103 | async with create_connected_server_and_client_session( 104 | make_server() 105 | ) as client_session: 106 | async with anyio.create_task_group() as tg: 107 | tg.start_soon(make_request, client_session) 108 | 109 | # Wait for the request to be in-flight 110 | with anyio.fail_after(1): # Timeout after 1 second 111 | await ev_tool_called.wait() 112 | 113 | # Send cancellation notification 114 | assert request_id is not None 115 | await client_session.send_notification( 116 | ClientNotification( 117 | CancelledNotification( 118 | method="notifications/cancelled", 119 | params=CancelledNotificationParams(requestId=request_id), 120 | ) 121 | ) 122 | ) 123 | 124 | # Give cancellation time to process 125 | with anyio.fail_after(1): 126 | await ev_cancelled.wait() 127 | -------------------------------------------------------------------------------- /tests/test_examples.py: -------------------------------------------------------------------------------- 1 | """Tests for example servers""" 2 | 3 | import pytest 4 | from pytest_examples import CodeExample, EvalExample, find_examples 5 | 6 | from mcp.shared.memory import ( 7 | create_connected_server_and_client_session as client_session, 8 | ) 9 | from mcp.types import TextContent, TextResourceContents 10 | 11 | 12 | @pytest.mark.anyio 13 | async def test_simple_echo(): 14 | """Test the simple echo server""" 15 | from examples.fastmcp.simple_echo import mcp 16 | 17 | async with client_session(mcp._mcp_server) as client: 18 | result = await client.call_tool("echo", {"text": "hello"}) 19 | assert len(result.content) == 1 20 | content = result.content[0] 21 | assert isinstance(content, TextContent) 22 | assert content.text == "hello" 23 | 24 | 25 | @pytest.mark.anyio 26 | async def test_complex_inputs(): 27 | """Test the complex inputs server""" 28 | from examples.fastmcp.complex_inputs import mcp 29 | 30 | async with client_session(mcp._mcp_server) as client: 31 | tank = {"shrimp": [{"name": "bob"}, {"name": "alice"}]} 32 | result = await client.call_tool( 33 | "name_shrimp", {"tank": tank, "extra_names": ["charlie"]} 34 | ) 35 | assert len(result.content) == 3 36 | assert isinstance(result.content[0], TextContent) 37 | assert isinstance(result.content[1], TextContent) 38 | assert isinstance(result.content[2], TextContent) 39 | assert result.content[0].text == "bob" 40 | assert result.content[1].text == "alice" 41 | assert result.content[2].text == "charlie" 42 | 43 | 44 | @pytest.mark.anyio 45 | async def test_desktop(monkeypatch): 46 | """Test the desktop server""" 47 | from pathlib import Path 48 | 49 | from pydantic import AnyUrl 50 | 51 | from examples.fastmcp.desktop import mcp 52 | 53 | # Mock desktop directory listing 54 | mock_files = [Path("/fake/path/file1.txt"), Path("/fake/path/file2.txt")] 55 | monkeypatch.setattr(Path, "iterdir", lambda self: mock_files) 56 | monkeypatch.setattr(Path, "home", lambda: Path("/fake/home")) 57 | 58 | async with client_session(mcp._mcp_server) as client: 59 | # Test the add function 60 | result = await client.call_tool("add", {"a": 1, "b": 2}) 61 | assert len(result.content) == 1 62 | content = result.content[0] 63 | assert isinstance(content, TextContent) 64 | assert content.text == "3" 65 | 66 | # Test the desktop resource 67 | result = await client.read_resource(AnyUrl("dir://desktop")) 68 | assert len(result.contents) == 1 69 | content = result.contents[0] 70 | assert isinstance(content, TextResourceContents) 71 | assert isinstance(content.text, str) 72 | assert "/fake/path/file1.txt" in content.text 73 | assert "/fake/path/file2.txt" in content.text 74 | 75 | 76 | @pytest.mark.parametrize("example", find_examples("README.md"), ids=str) 77 | def test_docs_examples(example: CodeExample, eval_example: EvalExample): 78 | ruff_ignore: list[str] = ["F841", "I001"] 79 | 80 | eval_example.set_config( 81 | ruff_ignore=ruff_ignore, target_version="py310", line_length=88 82 | ) 83 | 84 | if eval_example.update_examples: # pragma: no cover 85 | eval_example.format(example) 86 | else: 87 | eval_example.lint(example) 88 | -------------------------------------------------------------------------------- /tests/test_types.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from mcp.types import ( 4 | LATEST_PROTOCOL_VERSION, 5 | ClientRequest, 6 | JSONRPCMessage, 7 | JSONRPCRequest, 8 | ) 9 | 10 | 11 | @pytest.mark.anyio 12 | async def test_jsonrpc_request(): 13 | json_data = { 14 | "jsonrpc": "2.0", 15 | "id": 1, 16 | "method": "initialize", 17 | "params": { 18 | "protocolVersion": LATEST_PROTOCOL_VERSION, 19 | "capabilities": {"batch": None, "sampling": None}, 20 | "clientInfo": {"name": "mcp", "version": "0.1.0"}, 21 | }, 22 | } 23 | 24 | request = JSONRPCMessage.model_validate(json_data) 25 | assert isinstance(request.root, JSONRPCRequest) 26 | ClientRequest.model_validate(request.model_dump(by_alias=True, exclude_none=True)) 27 | 28 | assert request.root.jsonrpc == "2.0" 29 | assert request.root.id == 1 30 | assert request.root.method == "initialize" 31 | assert request.root.params is not None 32 | assert request.root.params["protocolVersion"] == LATEST_PROTOCOL_VERSION 33 | --------------------------------------------------------------------------------