├── .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-auth-client │ │ ├── README.md │ │ ├── mcp_simple_auth_client │ │ │ ├── __init__.py │ │ │ └── main.py │ │ ├── pyproject.toml │ │ └── uv.lock │ └── 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-auth │ ├── README.md │ ├── mcp_simple_auth │ │ ├── __init__.py │ │ ├── __main__.py │ │ └── server.py │ └── pyproject.toml │ ├── 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-streamablehttp-stateless │ ├── README.md │ ├── mcp_simple_streamablehttp_stateless │ │ ├── __init__.py │ │ ├── __main__.py │ │ └── server.py │ └── pyproject.toml │ ├── simple-streamablehttp │ ├── README.md │ ├── mcp_simple_streamablehttp │ │ ├── __init__.py │ │ ├── __main__.py │ │ ├── event_store.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 │ ├── auth.py │ ├── session.py │ ├── session_group.py │ ├── sse.py │ ├── stdio │ │ ├── __init__.py │ │ └── win32.py │ ├── streamable_http.py │ └── websocket.py │ ├── py.typed │ ├── server │ ├── __init__.py │ ├── __main__.py │ ├── auth │ │ ├── __init__.py │ │ ├── errors.py │ │ ├── handlers │ │ │ ├── __init__.py │ │ │ ├── authorize.py │ │ │ ├── metadata.py │ │ │ ├── register.py │ │ │ ├── revoke.py │ │ │ └── token.py │ │ ├── json_response.py │ │ ├── middleware │ │ │ ├── __init__.py │ │ │ ├── auth_context.py │ │ │ ├── bearer_auth.py │ │ │ └── client_auth.py │ │ ├── provider.py │ │ ├── routes.py │ │ └── settings.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 │ ├── streamable_http.py │ ├── streamable_http_manager.py │ ├── streaming_asgi_transport.py │ └── websocket.py │ ├── shared │ ├── __init__.py │ ├── _httpx_utils.py │ ├── auth.py │ ├── context.py │ ├── exceptions.py │ ├── memory.py │ ├── message.py │ ├── progress.py │ ├── session.py │ └── version.py │ └── types.py ├── tests ├── __init__.py ├── client │ ├── __init__.py │ ├── conftest.py │ ├── test_auth.py │ ├── test_config.py │ ├── test_list_methods_cursor.py │ ├── test_list_roots_callback.py │ ├── test_logging_callback.py │ ├── test_resource_cleanup.py │ ├── test_sampling_callback.py │ ├── test_session.py │ ├── test_session_group.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 │ ├── auth │ │ ├── middleware │ │ │ ├── test_auth_context.py │ │ │ └── test_bearer_auth.py │ │ └── test_error_handling.py │ ├── fastmcp │ │ ├── __init__.py │ │ ├── auth │ │ │ ├── __init__.py │ │ │ └── test_auth_integration.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_integration.py │ │ ├── test_parameter_descriptions.py │ │ ├── test_server.py │ │ └── test_tool_manager.py │ ├── test_lifespan.py │ ├── test_lowlevel_tool_annotations.py │ ├── test_read_resource.py │ ├── test_session.py │ ├── test_stdio.py │ └── test_streamable_http_manager.py ├── shared │ ├── test_httpx_utils.py │ ├── test_memory.py │ ├── test_progress_notifications.py │ ├── test_session.py │ ├── test_sse.py │ ├── test_streamable_http.py │ └── test_ws.py ├── test_examples.py └── test_types.py └── uv.lock /.git-blame-ignore-revs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/544176770b53e6a0ae8c413d3b6c5116421f67df/.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 | version: 0.7.2 23 | 24 | - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV 25 | - uses: actions/cache@v4 26 | with: 27 | key: mkdocs-material-${{ env.cache_id }} 28 | path: .cache 29 | restore-keys: | 30 | mkdocs-material- 31 | 32 | - run: uv sync --frozen --group docs 33 | - run: uv run --no-sync mkdocs gh-deploy --force 34 | -------------------------------------------------------------------------------- /.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 | version: 0.7.2 20 | 21 | - name: Set up Python 3.12 22 | run: uv python install 3.12 23 | 24 | - name: Build 25 | run: uv build 26 | 27 | - name: Upload artifacts 28 | uses: actions/upload-artifact@v4 29 | with: 30 | name: release-dists 31 | path: dist/ 32 | 33 | checks: 34 | uses: ./.github/workflows/shared.yml 35 | 36 | pypi-publish: 37 | name: Upload release to PyPI 38 | runs-on: ubuntu-latest 39 | environment: release 40 | needs: 41 | - release-build 42 | permissions: 43 | id-token: write # IMPORTANT: this permission is mandatory for trusted publishing 44 | 45 | steps: 46 | - name: Retrieve release distributions 47 | uses: actions/download-artifact@v4 48 | with: 49 | name: release-dists 50 | path: dist/ 51 | 52 | - name: Publish package distributions to PyPI 53 | uses: pypa/gh-action-pypi-publish@release/v1 54 | 55 | docs-publish: 56 | runs-on: ubuntu-latest 57 | needs: ["pypi-publish"] 58 | permissions: 59 | contents: write 60 | steps: 61 | - uses: actions/checkout@v4 62 | - name: Configure Git Credentials 63 | run: | 64 | git config user.name github-actions[bot] 65 | git config user.email 41898282+github-actions[bot]@users.noreply.github.com 66 | 67 | - name: Install uv 68 | uses: astral-sh/setup-uv@v3 69 | with: 70 | enable-cache: true 71 | version: 0.7.2 72 | 73 | - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV 74 | - uses: actions/cache@v4 75 | with: 76 | key: mkdocs-material-${{ env.cache_id }} 77 | path: .cache 78 | restore-keys: | 79 | mkdocs-material- 80 | 81 | - run: uv sync --frozen --group docs 82 | - run: uv run --no-sync mkdocs gh-deploy --force 83 | -------------------------------------------------------------------------------- /.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 | version: 0.7.2 17 | 18 | - name: Install the project 19 | run: uv sync --frozen --all-extras --dev --python 3.12 20 | 21 | - name: Run ruff format check 22 | run: uv run --no-sync ruff check . 23 | 24 | typecheck: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v4 28 | 29 | - name: Install uv 30 | uses: astral-sh/setup-uv@v3 31 | with: 32 | enable-cache: true 33 | version: 0.7.2 34 | 35 | - name: Install the project 36 | run: uv sync --frozen --all-extras --dev --python 3.12 37 | 38 | - name: Run pyright 39 | run: uv run --no-sync pyright 40 | 41 | test: 42 | runs-on: ${{ matrix.os }} 43 | strategy: 44 | matrix: 45 | python-version: ["3.10", "3.11", "3.12", "3.13"] 46 | os: [ubuntu-latest, windows-latest] 47 | 48 | steps: 49 | - uses: actions/checkout@v4 50 | 51 | - name: Install uv 52 | uses: astral-sh/setup-uv@v3 53 | with: 54 | enable-cache: true 55 | version: 0.7.2 56 | 57 | - name: Install the project 58 | run: uv sync --frozen --all-extras --dev --python ${{ matrix.python-version }} 59 | 60 | - name: Run pytest 61 | run: uv run --no-sync pytest 62 | continue-on-error: true 63 | -------------------------------------------------------------------------------- /.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 | .windsurfrules 170 | **/CLAUDE.local.md 171 | -------------------------------------------------------------------------------- /.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 --frozen 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 --frozen ruff format .` 58 | - Check: `uv run --frozen ruff check .` 59 | - Fix: `uv run --frozen 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 --frozen 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 | - Pytest: 108 | - If the tests aren't finding the anyio pytest mark, try adding PYTEST_DISABLE_PLUGIN_AUTOLOAD="" 109 | to the start of the pytest run command eg: 110 | `PYTEST_DISABLE_PLUGIN_AUTOLOAD="" uv run --frozen pytest` 111 | 112 | 3. Best Practices 113 | - Check git status before commits 114 | - Run formatters before type checks 115 | - Keep changes minimal 116 | - Follow existing patterns 117 | - Document public APIs 118 | - Test thoroughly 119 | -------------------------------------------------------------------------------- /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-auth-client/README.md: -------------------------------------------------------------------------------- 1 | # Simple Auth Client Example 2 | 3 | A demonstration of how to use the MCP Python SDK with OAuth authentication over streamable HTTP or SSE transport. 4 | 5 | ## Features 6 | 7 | - OAuth 2.0 authentication with PKCE 8 | - Support for both StreamableHTTP and SSE transports 9 | - Interactive command-line interface 10 | 11 | ## Installation 12 | 13 | ```bash 14 | cd examples/clients/simple-auth-client 15 | uv sync --reinstall 16 | ``` 17 | 18 | ## Usage 19 | 20 | ### 1. Start an MCP server with OAuth support 21 | 22 | ```bash 23 | # Example with mcp-simple-auth 24 | cd path/to/mcp-simple-auth 25 | uv run mcp-simple-auth --transport streamable-http --port 3001 26 | ``` 27 | 28 | ### 2. Run the client 29 | 30 | ```bash 31 | uv run mcp-simple-auth-client 32 | 33 | # Or with custom server URL 34 | MCP_SERVER_PORT=3001 uv run mcp-simple-auth-client 35 | 36 | # Use SSE transport 37 | MCP_TRANSPORT_TYPE=sse uv run mcp-simple-auth-client 38 | ``` 39 | 40 | ### 3. Complete OAuth flow 41 | 42 | The client will open your browser for authentication. After completing OAuth, you can use commands: 43 | 44 | - `list` - List available tools 45 | - `call [args]` - Call a tool with optional JSON arguments 46 | - `quit` - Exit 47 | 48 | ## Example 49 | 50 | ``` 51 | 🔐 Simple MCP Auth Client 52 | Connecting to: http://localhost:3001 53 | 54 | Please visit the following URL to authorize the application: 55 | http://localhost:3001/authorize?response_type=code&client_id=... 56 | 57 | ✅ Connected to MCP server at http://localhost:3001 58 | 59 | mcp> list 60 | 📋 Available tools: 61 | 1. echo - Echo back the input text 62 | 63 | mcp> call echo {"text": "Hello, world!"} 64 | 🔧 Tool 'echo' result: 65 | Hello, world! 66 | 67 | mcp> quit 68 | 👋 Goodbye! 69 | ``` 70 | 71 | ## Configuration 72 | 73 | - `MCP_SERVER_PORT` - Server URL (default: 8000) 74 | - `MCP_TRANSPORT_TYPE` - Transport type: `streamable_http` (default) or `sse` 75 | -------------------------------------------------------------------------------- /examples/clients/simple-auth-client/mcp_simple_auth_client/__init__.py: -------------------------------------------------------------------------------- 1 | """Simple OAuth client for MCP simple-auth server.""" 2 | -------------------------------------------------------------------------------- /examples/clients/simple-auth-client/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "mcp-simple-auth-client" 3 | version = "0.1.0" 4 | description = "A simple OAuth client for the MCP simple-auth server" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | authors = [{ name = "Anthropic" }] 8 | keywords = ["mcp", "oauth", "client", "auth"] 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 | "click>=8.0.0", 19 | "mcp>=1.0.0", 20 | ] 21 | 22 | [project.scripts] 23 | mcp-simple-auth-client = "mcp_simple_auth_client.main:cli" 24 | 25 | [build-system] 26 | requires = ["hatchling"] 27 | build-backend = "hatchling.build" 28 | 29 | [tool.hatch.build.targets.wheel] 30 | packages = ["mcp_simple_auth_client"] 31 | 32 | [tool.pyright] 33 | include = ["mcp_simple_auth_client"] 34 | venvPath = "." 35 | venv = ".venv" 36 | 37 | [tool.ruff.lint] 38 | select = ["E", "F", "I"] 39 | ignore = [] 40 | 41 | [tool.ruff] 42 | line-length = 88 43 | target-version = "py310" 44 | 45 | [tool.uv] 46 | dev-dependencies = ["pyright>=1.1.379", "pytest>=8.3.3", "ruff>=0.6.9"] 47 | 48 | [tool.uv.sources] 49 | mcp = { path = "../../../" } 50 | 51 | [[tool.uv.index]] 52 | url = "https://pypi.org/simple" 53 | -------------------------------------------------------------------------------- /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 | **Note:** The current implementation is configured to use the Groq API endpoint (`https://api.groq.com/openai/v1/chat/completions`) with the `llama-3.2-90b-vision-preview` model. If you plan to use a different LLM provider, you'll need to modify the `LLMClient` class in `main.py` to use the appropriate endpoint URL and model parameters. 29 | 30 | 3. **Configure servers:** 31 | 32 | The `servers_config.json` follows the same structure as Claude Desktop, allowing for easy integration of multiple servers. 33 | Here's an example: 34 | 35 | ```json 36 | { 37 | "mcpServers": { 38 | "sqlite": { 39 | "command": "uvx", 40 | "args": ["mcp-server-sqlite", "--db-path", "./test.db"] 41 | }, 42 | "puppeteer": { 43 | "command": "npx", 44 | "args": ["-y", "@modelcontextprotocol/server-puppeteer"] 45 | } 46 | } 47 | } 48 | ``` 49 | Environment variables are supported as well. Pass them as you would with the Claude Desktop App. 50 | 51 | Example: 52 | ```json 53 | { 54 | "mcpServers": { 55 | "server_name": { 56 | "command": "uvx", 57 | "args": ["mcp-server-name", "--additional-args"], 58 | "env": { 59 | "API_KEY": "your_api_key_here" 60 | } 61 | } 62 | } 63 | } 64 | ``` 65 | 66 | ## Usage 67 | 68 | 1. **Run the client:** 69 | 70 | ```bash 71 | python main.py 72 | ``` 73 | 74 | 2. **Interact with the assistant:** 75 | 76 | The assistant will automatically detect available tools and can respond to queries based on the tools provided by the configured servers. 77 | 78 | 3. **Exit the session:** 79 | 80 | Type `quit` or `exit` to end the session. 81 | 82 | ## Architecture 83 | 84 | - **Tool Discovery**: Tools are automatically discovered from configured servers. 85 | - **System Prompt**: Tools are dynamically included in the system prompt, allowing the LLM to understand available capabilities. 86 | - **Server Integration**: Supports any MCP-compatible server, tested with various server implementations including Uvicorn and Node.js. 87 | 88 | ### Class Structure 89 | - **Configuration**: Manages environment variables and server configurations 90 | - **Server**: Handles MCP server initialization, tool discovery, and execution 91 | - **Tool**: Represents individual tools with their properties and formatting 92 | - **LLMClient**: Manages communication with the LLM provider 93 | - **ChatSession**: Orchestrates the interaction between user, LLM, and tools 94 | 95 | ### Logic Flow 96 | 97 | 1. **Tool Integration**: 98 | - Tools are dynamically discovered from MCP servers 99 | - Tool descriptions are automatically included in system prompt 100 | - Tool execution is handled through standardized MCP protocol 101 | 102 | 2. **Runtime Flow**: 103 | - User input is received 104 | - Input is sent to LLM with context of available tools 105 | - LLM response is parsed: 106 | - If it's a tool call → execute tool and return result 107 | - If it's a direct response → return to user 108 | - Tool results are sent back to LLM for interpretation 109 | - Final response is presented to user 110 | 111 | 112 | -------------------------------------------------------------------------------- /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/544176770b53e6a0ae8c413d3b6c5116421f67df/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-auth/README.md: -------------------------------------------------------------------------------- 1 | # Simple MCP Server with GitHub OAuth Authentication 2 | 3 | This is a simple example of an MCP server with GitHub OAuth authentication. It demonstrates the essential components needed for OAuth integration with just a single tool. 4 | 5 | This is just an example of a server that uses auth, an official GitHub mcp server is [here](https://github.com/github/github-mcp-server) 6 | 7 | ## Overview 8 | 9 | This simple demo to show to set up a server with: 10 | - GitHub OAuth2 authorization flow 11 | - Single tool: `get_user_profile` to retrieve GitHub user information 12 | 13 | 14 | ## Prerequisites 15 | 16 | 1. Create a GitHub OAuth App: 17 | - Go to GitHub Settings > Developer settings > OAuth Apps > New OAuth App 18 | - Application name: Any name (e.g., "Simple MCP Auth Demo") 19 | - Homepage URL: `http://localhost:8000` 20 | - Authorization callback URL: `http://localhost:8000/github/callback` 21 | - Click "Register application" 22 | - Note down your Client ID and Client Secret 23 | 24 | ## Required Environment Variables 25 | 26 | You MUST set these environment variables before running the server: 27 | 28 | ```bash 29 | export MCP_GITHUB_GITHUB_CLIENT_ID="your_client_id_here" 30 | export MCP_GITHUB_GITHUB_CLIENT_SECRET="your_client_secret_here" 31 | ``` 32 | 33 | The server will not start without these environment variables properly set. 34 | 35 | 36 | ## Running the Server 37 | 38 | ```bash 39 | # Set environment variables first (see above) 40 | 41 | # Run the server 42 | uv run mcp-simple-auth 43 | ``` 44 | 45 | The server will start on `http://localhost:8000`. 46 | 47 | ### Transport Options 48 | 49 | This server supports multiple transport protocols that can run on the same port: 50 | 51 | #### SSE (Server-Sent Events) - Default 52 | ```bash 53 | uv run mcp-simple-auth 54 | # or explicitly: 55 | uv run mcp-simple-auth --transport sse 56 | ``` 57 | 58 | SSE transport provides endpoint: 59 | - `/sse` 60 | 61 | #### Streamable HTTP 62 | ```bash 63 | uv run mcp-simple-auth --transport streamable-http 64 | ``` 65 | 66 | Streamable HTTP transport provides endpoint: 67 | - `/mcp` 68 | 69 | 70 | This ensures backward compatibility without needing multiple server instances. When using SSE transport (`--transport sse`), only the `/sse` endpoint is available. 71 | 72 | ## Available Tool 73 | 74 | ### get_user_profile 75 | 76 | The only tool in this simple example. Returns the authenticated user's GitHub profile information. 77 | 78 | **Required scope**: `user` 79 | 80 | **Returns**: GitHub user profile data including username, email, bio, etc. 81 | 82 | 83 | ## Troubleshooting 84 | 85 | If the server fails to start, check: 86 | 1. Environment variables `MCP_GITHUB_GITHUB_CLIENT_ID` and `MCP_GITHUB_GITHUB_CLIENT_SECRET` are set 87 | 2. The GitHub OAuth app callback URL matches `http://localhost:8000/github/callback` 88 | 3. No other service is using port 8000 89 | 4. The transport specified is valid (`sse` or `streamable-http`) 90 | 91 | You can use [Inspector](https://github.com/modelcontextprotocol/inspector) to test Auth -------------------------------------------------------------------------------- /examples/servers/simple-auth/mcp_simple_auth/__init__.py: -------------------------------------------------------------------------------- 1 | """Simple MCP server with GitHub OAuth authentication.""" 2 | -------------------------------------------------------------------------------- /examples/servers/simple-auth/mcp_simple_auth/__main__.py: -------------------------------------------------------------------------------- 1 | """Main entry point for simple MCP server with GitHub OAuth authentication.""" 2 | 3 | import sys 4 | 5 | from mcp_simple_auth.server import main 6 | 7 | sys.exit(main()) # type: ignore[call-arg] 8 | -------------------------------------------------------------------------------- /examples/servers/simple-auth/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "mcp-simple-auth" 3 | version = "0.1.0" 4 | description = "A simple MCP server demonstrating OAuth authentication" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | authors = [{ name = "Anthropic, PBC." }] 8 | license = { text = "MIT" } 9 | dependencies = [ 10 | "anyio>=4.5", 11 | "click>=8.1.0", 12 | "httpx>=0.27", 13 | "mcp", 14 | "pydantic>=2.0", 15 | "pydantic-settings>=2.5.2", 16 | "sse-starlette>=1.6.1", 17 | "uvicorn>=0.23.1; sys_platform != 'emscripten'", 18 | ] 19 | 20 | [project.scripts] 21 | mcp-simple-auth = "mcp_simple_auth.server:main" 22 | 23 | [build-system] 24 | requires = ["hatchling"] 25 | build-backend = "hatchling.build" 26 | 27 | [tool.hatch.build.targets.wheel] 28 | packages = ["mcp_simple_auth"] 29 | 30 | [tool.uv] 31 | dev-dependencies = ["pyright>=1.1.391", "pytest>=8.3.4", "ruff>=0.8.5"] -------------------------------------------------------------------------------- /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()) # type: ignore[call-arg] 6 | -------------------------------------------------------------------------------- /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()) # type: ignore[call-arg] 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 AnyUrl 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=AnyUrl(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: AnyUrl) -> str | bytes: 39 | if uri.path is None: 40 | raise ValueError(f"Invalid resource path: {uri}") 41 | name = uri.path.replace(".txt", "").lstrip("/") 42 | 43 | if name not in SAMPLE_RESOURCES: 44 | raise ValueError(f"Unknown resource: {uri}") 45 | 46 | return SAMPLE_RESOURCES[name] 47 | 48 | if transport == "sse": 49 | from mcp.server.sse import SseServerTransport 50 | from starlette.applications import Starlette 51 | from starlette.responses import Response 52 | from starlette.routing import Mount, Route 53 | 54 | sse = SseServerTransport("/messages/") 55 | 56 | async def handle_sse(request): 57 | async with sse.connect_sse( 58 | request.scope, request.receive, request._send 59 | ) as streams: 60 | await app.run( 61 | streams[0], streams[1], app.create_initialization_options() 62 | ) 63 | return Response() 64 | 65 | starlette_app = Starlette( 66 | debug=True, 67 | routes=[ 68 | Route("/sse", endpoint=handle_sse, methods=["GET"]), 69 | Mount("/messages/", app=sse.handle_post_message), 70 | ], 71 | ) 72 | 73 | import uvicorn 74 | 75 | uvicorn.run(starlette_app, host="127.0.0.1", port=port) 76 | else: 77 | from mcp.server.stdio import stdio_server 78 | 79 | async def arun(): 80 | async with stdio_server() as streams: 81 | await app.run( 82 | streams[0], streams[1], app.create_initialization_options() 83 | ) 84 | 85 | anyio.run(arun) 86 | 87 | return 0 88 | -------------------------------------------------------------------------------- /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-streamablehttp-stateless/README.md: -------------------------------------------------------------------------------- 1 | # MCP Simple StreamableHttp Stateless Server Example 2 | 3 | A stateless MCP server example demonstrating the StreamableHttp transport without maintaining session state. This example is ideal for understanding how to deploy MCP servers in multi-node environments where requests can be routed to any instance. 4 | 5 | ## Features 6 | 7 | - Uses the StreamableHTTP transport in stateless mode (mcp_session_id=None) 8 | - Each request creates a new ephemeral connection 9 | - No session state maintained between requests 10 | - Task lifecycle scoped to individual requests 11 | - Suitable for deployment in multi-node environments 12 | 13 | 14 | ## Usage 15 | 16 | Start the server: 17 | 18 | ```bash 19 | # Using default port 3000 20 | uv run mcp-simple-streamablehttp-stateless 21 | 22 | # Using custom port 23 | uv run mcp-simple-streamablehttp-stateless --port 3000 24 | 25 | # Custom logging level 26 | uv run mcp-simple-streamablehttp-stateless --log-level DEBUG 27 | 28 | # Enable JSON responses instead of SSE streams 29 | uv run mcp-simple-streamablehttp-stateless --json-response 30 | ``` 31 | 32 | The server exposes a tool named "start-notification-stream" that accepts three arguments: 33 | 34 | - `interval`: Time between notifications in seconds (e.g., 1.0) 35 | - `count`: Number of notifications to send (e.g., 5) 36 | - `caller`: Identifier string for the caller 37 | 38 | 39 | ## Client 40 | 41 | You can connect to this server using an HTTP client. For now, only the TypeScript SDK has streamable HTTP client examples, or you can use [Inspector](https://github.com/modelcontextprotocol/inspector) for testing. -------------------------------------------------------------------------------- /examples/servers/simple-streamablehttp-stateless/mcp_simple_streamablehttp_stateless/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/544176770b53e6a0ae8c413d3b6c5116421f67df/examples/servers/simple-streamablehttp-stateless/mcp_simple_streamablehttp_stateless/__init__.py -------------------------------------------------------------------------------- /examples/servers/simple-streamablehttp-stateless/mcp_simple_streamablehttp_stateless/__main__.py: -------------------------------------------------------------------------------- 1 | from .server import main 2 | 3 | if __name__ == "__main__": 4 | # Click will handle CLI arguments 5 | import sys 6 | 7 | sys.exit(main()) # type: ignore[call-arg] 8 | -------------------------------------------------------------------------------- /examples/servers/simple-streamablehttp-stateless/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "mcp-simple-streamablehttp-stateless" 3 | version = "0.1.0" 4 | description = "A simple MCP server exposing a StreamableHttp transport in stateless mode" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | authors = [{ name = "Anthropic, PBC." }] 8 | keywords = ["mcp", "llm", "automation", "web", "fetch", "http", "streamable", "stateless"] 9 | license = { text = "MIT" } 10 | dependencies = ["anyio>=4.5", "click>=8.1.0", "httpx>=0.27", "mcp", "starlette", "uvicorn"] 11 | 12 | [project.scripts] 13 | mcp-simple-streamablehttp-stateless = "mcp_simple_streamablehttp_stateless.server:main" 14 | 15 | [build-system] 16 | requires = ["hatchling"] 17 | build-backend = "hatchling.build" 18 | 19 | [tool.hatch.build.targets.wheel] 20 | packages = ["mcp_simple_streamablehttp_stateless"] 21 | 22 | [tool.pyright] 23 | include = ["mcp_simple_streamablehttp_stateless"] 24 | venvPath = "." 25 | venv = ".venv" 26 | 27 | [tool.ruff.lint] 28 | select = ["E", "F", "I"] 29 | ignore = [] 30 | 31 | [tool.ruff] 32 | line-length = 88 33 | target-version = "py310" 34 | 35 | [tool.uv] 36 | dev-dependencies = ["pyright>=1.1.378", "pytest>=8.3.3", "ruff>=0.6.9"] -------------------------------------------------------------------------------- /examples/servers/simple-streamablehttp/README.md: -------------------------------------------------------------------------------- 1 | # MCP Simple StreamableHttp Server Example 2 | 3 | A simple MCP server example demonstrating the StreamableHttp transport, which enables HTTP-based communication with MCP servers using streaming. 4 | 5 | ## Features 6 | 7 | - Uses the StreamableHTTP transport for server-client communication 8 | - Supports REST API operations (POST, GET, DELETE) for `/mcp` endpoint 9 | - Task management with anyio task groups 10 | - Ability to send multiple notifications over time to the client 11 | - Proper resource cleanup and lifespan management 12 | - Resumability support via InMemoryEventStore 13 | 14 | ## Usage 15 | 16 | Start the server on the default or custom port: 17 | 18 | ```bash 19 | 20 | # Using custom port 21 | uv run mcp-simple-streamablehttp --port 3000 22 | 23 | # Custom logging level 24 | uv run mcp-simple-streamablehttp --log-level DEBUG 25 | 26 | # Enable JSON responses instead of SSE streams 27 | uv run mcp-simple-streamablehttp --json-response 28 | ``` 29 | 30 | The server exposes a tool named "start-notification-stream" that accepts three arguments: 31 | 32 | - `interval`: Time between notifications in seconds (e.g., 1.0) 33 | - `count`: Number of notifications to send (e.g., 5) 34 | - `caller`: Identifier string for the caller 35 | 36 | ## Resumability Support 37 | 38 | This server includes resumability support through the InMemoryEventStore. This enables clients to: 39 | 40 | - Reconnect to the server after a disconnection 41 | - Resume event streaming from where they left off using the Last-Event-ID header 42 | 43 | 44 | The server will: 45 | - Generate unique event IDs for each SSE message 46 | - Store events in memory for later replay 47 | - Replay missed events when a client reconnects with a Last-Event-ID header 48 | 49 | Note: The InMemoryEventStore is designed for demonstration purposes only. For production use, consider implementing a persistent storage solution. 50 | 51 | 52 | 53 | ## Client 54 | 55 | You can connect to this server using an HTTP client, for now only Typescript SDK has streamable HTTP client examples or you can use [Inspector](https://github.com/modelcontextprotocol/inspector) -------------------------------------------------------------------------------- /examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/544176770b53e6a0ae8c413d3b6c5116421f67df/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/__init__.py -------------------------------------------------------------------------------- /examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/__main__.py: -------------------------------------------------------------------------------- 1 | from .server import main 2 | 3 | if __name__ == "__main__": 4 | main() # type: ignore[call-arg] 5 | -------------------------------------------------------------------------------- /examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/event_store.py: -------------------------------------------------------------------------------- 1 | """ 2 | In-memory event store for demonstrating resumability functionality. 3 | 4 | This is a simple implementation intended for examples and testing, 5 | not for production use where a persistent storage solution would be more appropriate. 6 | """ 7 | 8 | import logging 9 | from collections import deque 10 | from dataclasses import dataclass 11 | from uuid import uuid4 12 | 13 | from mcp.server.streamable_http import ( 14 | EventCallback, 15 | EventId, 16 | EventMessage, 17 | EventStore, 18 | StreamId, 19 | ) 20 | from mcp.types import JSONRPCMessage 21 | 22 | logger = logging.getLogger(__name__) 23 | 24 | 25 | @dataclass 26 | class EventEntry: 27 | """ 28 | Represents an event entry in the event store. 29 | """ 30 | 31 | event_id: EventId 32 | stream_id: StreamId 33 | message: JSONRPCMessage 34 | 35 | 36 | class InMemoryEventStore(EventStore): 37 | """ 38 | Simple in-memory implementation of the EventStore interface for resumability. 39 | This is primarily intended for examples and testing, not for production use 40 | where a persistent storage solution would be more appropriate. 41 | 42 | This implementation keeps only the last N events per stream for memory efficiency. 43 | """ 44 | 45 | def __init__(self, max_events_per_stream: int = 100): 46 | """Initialize the event store. 47 | 48 | Args: 49 | max_events_per_stream: Maximum number of events to keep per stream 50 | """ 51 | self.max_events_per_stream = max_events_per_stream 52 | # for maintaining last N events per stream 53 | self.streams: dict[StreamId, deque[EventEntry]] = {} 54 | # event_id -> EventEntry for quick lookup 55 | self.event_index: dict[EventId, EventEntry] = {} 56 | 57 | async def store_event( 58 | self, stream_id: StreamId, message: JSONRPCMessage 59 | ) -> EventId: 60 | """Stores an event with a generated event ID.""" 61 | event_id = str(uuid4()) 62 | event_entry = EventEntry( 63 | event_id=event_id, stream_id=stream_id, message=message 64 | ) 65 | 66 | # Get or create deque for this stream 67 | if stream_id not in self.streams: 68 | self.streams[stream_id] = deque(maxlen=self.max_events_per_stream) 69 | 70 | # If deque is full, the oldest event will be automatically removed 71 | # We need to remove it from the event_index as well 72 | if len(self.streams[stream_id]) == self.max_events_per_stream: 73 | oldest_event = self.streams[stream_id][0] 74 | self.event_index.pop(oldest_event.event_id, None) 75 | 76 | # Add new event 77 | self.streams[stream_id].append(event_entry) 78 | self.event_index[event_id] = event_entry 79 | 80 | return event_id 81 | 82 | async def replay_events_after( 83 | self, 84 | last_event_id: EventId, 85 | send_callback: EventCallback, 86 | ) -> StreamId | None: 87 | """Replays events that occurred after the specified event ID.""" 88 | if last_event_id not in self.event_index: 89 | logger.warning(f"Event ID {last_event_id} not found in store") 90 | return None 91 | 92 | # Get the stream and find events after the last one 93 | last_event = self.event_index[last_event_id] 94 | stream_id = last_event.stream_id 95 | stream_events = self.streams.get(last_event.stream_id, deque()) 96 | 97 | # Events in deque are already in chronological order 98 | found_last = False 99 | for event in stream_events: 100 | if found_last: 101 | await send_callback(EventMessage(event.message, event.event_id)) 102 | elif event.event_id == last_event_id: 103 | found_last = True 104 | 105 | return stream_id 106 | -------------------------------------------------------------------------------- /examples/servers/simple-streamablehttp/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "mcp-simple-streamablehttp" 3 | version = "0.1.0" 4 | description = "A simple MCP server exposing a StreamableHttp transport for testing" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | authors = [{ name = "Anthropic, PBC." }] 8 | keywords = ["mcp", "llm", "automation", "web", "fetch", "http", "streamable"] 9 | license = { text = "MIT" } 10 | dependencies = ["anyio>=4.5", "click>=8.1.0", "httpx>=0.27", "mcp", "starlette", "uvicorn"] 11 | 12 | [project.scripts] 13 | mcp-simple-streamablehttp = "mcp_simple_streamablehttp.server:main" 14 | 15 | [build-system] 16 | requires = ["hatchling"] 17 | build-backend = "hatchling.build" 18 | 19 | [tool.hatch.build.targets.wheel] 20 | packages = ["mcp_simple_streamablehttp"] 21 | 22 | [tool.pyright] 23 | include = ["mcp_simple_streamablehttp"] 24 | venvPath = "." 25 | venv = ".venv" 26 | 27 | [tool.ruff.lint] 28 | select = ["E", "F", "I"] 29 | ignore = [] 30 | 31 | [tool.ruff] 32 | line-length = 88 33 | target-version = "py310" 34 | 35 | [tool.uv] 36 | dev-dependencies = ["pyright>=1.1.378", "pytest>=8.3.3", "ruff>=0.6.9"] -------------------------------------------------------------------------------- /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()) # type: ignore[call-arg] 6 | -------------------------------------------------------------------------------- /examples/servers/simple-tool/mcp_simple_tool/server.py: -------------------------------------------------------------------------------- 1 | import anyio 2 | import click 3 | import mcp.types as types 4 | from mcp.server.lowlevel import Server 5 | from mcp.shared._httpx_utils import create_mcp_http_client 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 create_mcp_http_client(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.responses import Response 64 | from starlette.routing import Mount, Route 65 | 66 | sse = SseServerTransport("/messages/") 67 | 68 | async def handle_sse(request): 69 | async with sse.connect_sse( 70 | request.scope, request.receive, request._send 71 | ) as streams: 72 | await app.run( 73 | streams[0], streams[1], app.create_initialization_options() 74 | ) 75 | return Response() 76 | 77 | starlette_app = Starlette( 78 | debug=True, 79 | routes=[ 80 | Route("/sse", endpoint=handle_sse, methods=["GET"]), 81 | Mount("/messages/", app=sse.handle_post_message), 82 | ], 83 | ) 84 | 85 | import uvicorn 86 | 87 | uvicorn.run(starlette_app, host="127.0.0.1", port=port) 88 | else: 89 | from mcp.server.stdio import stdio_server 90 | 91 | async def arun(): 92 | async with stdio_server() as streams: 93 | await app.run( 94 | streams[0], streams[1], app.create_initialization_options() 95 | ) 96 | 97 | anyio.run(arun) 98 | 99 | return 0 100 | -------------------------------------------------------------------------------- /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 | "python-multipart>=0.0.9", 31 | "sse-starlette>=1.6.1", 32 | "pydantic-settings>=2.5.2", 33 | "uvicorn>=0.23.1; sys_platform != 'emscripten'", 34 | ] 35 | 36 | [project.optional-dependencies] 37 | rich = ["rich>=13.9.4"] 38 | cli = ["typer>=0.12.4", "python-dotenv>=1.0.0"] 39 | ws = ["websockets>=15.0.1"] 40 | 41 | [project.scripts] 42 | mcp = "mcp.cli:app [cli]" 43 | 44 | [tool.uv] 45 | resolution = "lowest-direct" 46 | default-groups = ["dev", "docs"] 47 | required-version = ">=0.7.2" 48 | 49 | [dependency-groups] 50 | dev = [ 51 | "pyright>=1.1.391", 52 | "pytest>=8.3.4", 53 | "ruff>=0.8.5", 54 | "trio>=0.26.2", 55 | "pytest-flakefinder>=1.1.0", 56 | "pytest-xdist>=3.6.1", 57 | "pytest-examples>=0.0.14", 58 | "pytest-pretty>=1.2.0", 59 | "inline-snapshot>=0.23.0", 60 | ] 61 | docs = [ 62 | "mkdocs>=1.6.1", 63 | "mkdocs-glightbox>=0.4.0", 64 | "mkdocs-material[imaging]>=9.5.45", 65 | "mkdocstrings-python>=1.12.2", 66 | ] 67 | 68 | [build-system] 69 | requires = ["hatchling", "uv-dynamic-versioning"] 70 | build-backend = "hatchling.build" 71 | 72 | [tool.hatch.version] 73 | source = "uv-dynamic-versioning" 74 | 75 | [tool.uv-dynamic-versioning] 76 | vcs = "git" 77 | style = "pep440" 78 | bump = true 79 | 80 | [project.urls] 81 | Homepage = "https://modelcontextprotocol.io" 82 | Repository = "https://github.com/modelcontextprotocol/python-sdk" 83 | Issues = "https://github.com/modelcontextprotocol/python-sdk/issues" 84 | 85 | [tool.hatch.build.targets.wheel] 86 | packages = ["src/mcp"] 87 | 88 | [tool.pyright] 89 | include = ["src/mcp", "tests", "examples/servers"] 90 | venvPath = "." 91 | venv = ".venv" 92 | strict = ["src/mcp/**/*.py"] 93 | 94 | [tool.ruff.lint] 95 | select = ["C4", "E", "F", "I", "PERF", "UP"] 96 | ignore = ["PERF203"] 97 | 98 | [tool.ruff] 99 | line-length = 88 100 | target-version = "py310" 101 | 102 | [tool.ruff.lint.per-file-ignores] 103 | "__init__.py" = ["F401"] 104 | "tests/server/fastmcp/test_func_metadata.py" = ["E501"] 105 | 106 | [tool.uv.workspace] 107 | members = ["examples/servers/*"] 108 | 109 | [tool.uv.sources] 110 | mcp = { workspace = true } 111 | 112 | [tool.pytest.ini_options] 113 | log_cli = true 114 | xfail_strict = true 115 | addopts = """ 116 | --color=yes 117 | --capture=fd 118 | --numprocesses auto 119 | """ 120 | filterwarnings = [ 121 | "error", 122 | # This should be fixed on Uvicorn's side. 123 | "ignore::DeprecationWarning:websockets", 124 | "ignore:websockets.server.WebSocketServerProtocol is deprecated:DeprecationWarning", 125 | "ignore:Returning str or bytes.*:DeprecationWarning:mcp.server.lowlevel" 126 | ] 127 | -------------------------------------------------------------------------------- /src/mcp/__init__.py: -------------------------------------------------------------------------------- 1 | from .client.session import ClientSession 2 | from .client.session_group import ClientSessionGroup 3 | from .client.stdio import StdioServerParameters, stdio_client 4 | from .server.session import ServerSession 5 | from .server.stdio import stdio_server 6 | from .shared.exceptions import McpError 7 | from .types import ( 8 | CallToolRequest, 9 | ClientCapabilities, 10 | ClientNotification, 11 | ClientRequest, 12 | ClientResult, 13 | CompleteRequest, 14 | CreateMessageRequest, 15 | CreateMessageResult, 16 | ErrorData, 17 | GetPromptRequest, 18 | GetPromptResult, 19 | Implementation, 20 | IncludeContext, 21 | InitializedNotification, 22 | InitializeRequest, 23 | InitializeResult, 24 | JSONRPCError, 25 | JSONRPCRequest, 26 | JSONRPCResponse, 27 | ListPromptsRequest, 28 | ListPromptsResult, 29 | ListResourcesRequest, 30 | ListResourcesResult, 31 | ListToolsResult, 32 | LoggingLevel, 33 | LoggingMessageNotification, 34 | Notification, 35 | PingRequest, 36 | ProgressNotification, 37 | PromptsCapability, 38 | ReadResourceRequest, 39 | ReadResourceResult, 40 | Resource, 41 | ResourcesCapability, 42 | ResourceUpdatedNotification, 43 | RootsCapability, 44 | SamplingMessage, 45 | ServerCapabilities, 46 | ServerNotification, 47 | ServerRequest, 48 | ServerResult, 49 | SetLevelRequest, 50 | StopReason, 51 | SubscribeRequest, 52 | Tool, 53 | ToolsCapability, 54 | UnsubscribeRequest, 55 | ) 56 | from .types import ( 57 | Role as SamplingRole, 58 | ) 59 | 60 | __all__ = [ 61 | "CallToolRequest", 62 | "ClientCapabilities", 63 | "ClientNotification", 64 | "ClientRequest", 65 | "ClientResult", 66 | "ClientSession", 67 | "ClientSessionGroup", 68 | "CreateMessageRequest", 69 | "CreateMessageResult", 70 | "ErrorData", 71 | "GetPromptRequest", 72 | "GetPromptResult", 73 | "Implementation", 74 | "IncludeContext", 75 | "InitializeRequest", 76 | "InitializeResult", 77 | "InitializedNotification", 78 | "JSONRPCError", 79 | "JSONRPCRequest", 80 | "ListPromptsRequest", 81 | "ListPromptsResult", 82 | "ListResourcesRequest", 83 | "ListResourcesResult", 84 | "ListToolsResult", 85 | "LoggingLevel", 86 | "LoggingMessageNotification", 87 | "McpError", 88 | "Notification", 89 | "PingRequest", 90 | "ProgressNotification", 91 | "PromptsCapability", 92 | "ReadResourceRequest", 93 | "ReadResourceResult", 94 | "ResourcesCapability", 95 | "ResourceUpdatedNotification", 96 | "Resource", 97 | "RootsCapability", 98 | "SamplingMessage", 99 | "SamplingRole", 100 | "ServerCapabilities", 101 | "ServerNotification", 102 | "ServerRequest", 103 | "ServerResult", 104 | "ServerSession", 105 | "SetLevelRequest", 106 | "StdioServerParameters", 107 | "StopReason", 108 | "SubscribeRequest", 109 | "Tool", 110 | "ToolsCapability", 111 | "UnsubscribeRequest", 112 | "stdio_client", 113 | "stdio_server", 114 | "CompleteRequest", 115 | "JSONRPCResponse", 116 | ] 117 | -------------------------------------------------------------------------------- /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/client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/544176770b53e6a0ae8c413d3b6c5116421f67df/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.message import SessionMessage 15 | from mcp.shared.session import RequestResponder 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[SessionMessage | Exception], 40 | write_stream: MemoryObjectSendStream[SessionMessage], 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 | from mcp.shared.message import SessionMessage 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | 18 | @asynccontextmanager 19 | async def websocket_client( 20 | url: str, 21 | ) -> AsyncGenerator[ 22 | tuple[ 23 | MemoryObjectReceiveStream[SessionMessage | Exception], 24 | MemoryObjectSendStream[SessionMessage], 25 | ], 26 | None, 27 | ]: 28 | """ 29 | WebSocket client transport for MCP, symmetrical to the server version. 30 | 31 | Connects to 'url' using the 'mcp' subprotocol, then yields: 32 | (read_stream, write_stream) 33 | 34 | - read_stream: As you read from this stream, you'll receive either valid 35 | JSONRPCMessage objects or Exception objects (when validation fails). 36 | - write_stream: Write JSONRPCMessage objects to this stream to send them 37 | over the WebSocket to the server. 38 | """ 39 | 40 | # Create two in-memory streams: 41 | # - One for incoming messages (read_stream, written by ws_reader) 42 | # - One for outgoing messages (write_stream, read by ws_writer) 43 | read_stream: MemoryObjectReceiveStream[SessionMessage | Exception] 44 | read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception] 45 | write_stream: MemoryObjectSendStream[SessionMessage] 46 | write_stream_reader: MemoryObjectReceiveStream[SessionMessage] 47 | 48 | read_stream_writer, read_stream = anyio.create_memory_object_stream(0) 49 | write_stream, write_stream_reader = anyio.create_memory_object_stream(0) 50 | 51 | # Connect using websockets, requesting the "mcp" subprotocol 52 | async with ws_connect(url, subprotocols=[Subprotocol("mcp")]) as ws: 53 | 54 | async def ws_reader(): 55 | """ 56 | Reads text messages from the WebSocket, parses them as JSON-RPC messages, 57 | and sends them into read_stream_writer. 58 | """ 59 | async with read_stream_writer: 60 | async for raw_text in ws: 61 | try: 62 | message = types.JSONRPCMessage.model_validate_json(raw_text) 63 | session_message = SessionMessage(message) 64 | await read_stream_writer.send(session_message) 65 | except ValidationError as exc: 66 | # If JSON parse or model validation fails, send the exception 67 | await read_stream_writer.send(exc) 68 | 69 | async def ws_writer(): 70 | """ 71 | Reads JSON-RPC messages from write_stream_reader and 72 | sends them to the server. 73 | """ 74 | async with write_stream_reader: 75 | async for session_message in write_stream_reader: 76 | # Convert to a dict, then to JSON 77 | msg_dict = session_message.message.model_dump( 78 | by_alias=True, mode="json", exclude_none=True 79 | ) 80 | await ws.send(json.dumps(msg_dict)) 81 | 82 | async with anyio.create_task_group() as tg: 83 | # Start reader and writer tasks 84 | tg.start_soon(ws_reader) 85 | tg.start_soon(ws_writer) 86 | 87 | # Yield the receive/send streams 88 | yield (read_stream, write_stream) 89 | 90 | # Once the caller's 'async with' block exits, we shut down 91 | tg.cancel_scope.cancel() 92 | -------------------------------------------------------------------------------- /src/mcp/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/544176770b53e6a0ae8c413d3b6c5116421f67df/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/auth/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | MCP OAuth server authorization components. 3 | """ 4 | -------------------------------------------------------------------------------- /src/mcp/server/auth/errors.py: -------------------------------------------------------------------------------- 1 | from pydantic import ValidationError 2 | 3 | 4 | def stringify_pydantic_error(validation_error: ValidationError) -> str: 5 | return "\n".join( 6 | f"{'.'.join(str(loc) for loc in e['loc'])}: {e['msg']}" 7 | for e in validation_error.errors() 8 | ) 9 | -------------------------------------------------------------------------------- /src/mcp/server/auth/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Request handlers for MCP authorization endpoints. 3 | """ 4 | -------------------------------------------------------------------------------- /src/mcp/server/auth/handlers/metadata.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from starlette.requests import Request 4 | from starlette.responses import Response 5 | 6 | from mcp.server.auth.json_response import PydanticJSONResponse 7 | from mcp.shared.auth import OAuthMetadata 8 | 9 | 10 | @dataclass 11 | class MetadataHandler: 12 | metadata: OAuthMetadata 13 | 14 | async def handle(self, request: Request) -> Response: 15 | return PydanticJSONResponse( 16 | content=self.metadata, 17 | headers={"Cache-Control": "public, max-age=3600"}, # Cache for 1 hour 18 | ) 19 | -------------------------------------------------------------------------------- /src/mcp/server/auth/handlers/revoke.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from functools import partial 3 | from typing import Any, Literal 4 | 5 | from pydantic import BaseModel, ValidationError 6 | from starlette.requests import Request 7 | from starlette.responses import Response 8 | 9 | from mcp.server.auth.errors import ( 10 | stringify_pydantic_error, 11 | ) 12 | from mcp.server.auth.json_response import PydanticJSONResponse 13 | from mcp.server.auth.middleware.client_auth import ( 14 | AuthenticationError, 15 | ClientAuthenticator, 16 | ) 17 | from mcp.server.auth.provider import ( 18 | AccessToken, 19 | OAuthAuthorizationServerProvider, 20 | RefreshToken, 21 | ) 22 | 23 | 24 | class RevocationRequest(BaseModel): 25 | """ 26 | # See https://datatracker.ietf.org/doc/html/rfc7009#section-2.1 27 | """ 28 | 29 | token: str 30 | token_type_hint: Literal["access_token", "refresh_token"] | None = None 31 | client_id: str 32 | client_secret: str | None 33 | 34 | 35 | class RevocationErrorResponse(BaseModel): 36 | error: Literal["invalid_request", "unauthorized_client"] 37 | error_description: str | None = None 38 | 39 | 40 | @dataclass 41 | class RevocationHandler: 42 | provider: OAuthAuthorizationServerProvider[Any, Any, Any] 43 | client_authenticator: ClientAuthenticator 44 | 45 | async def handle(self, request: Request) -> Response: 46 | """ 47 | Handler for the OAuth 2.0 Token Revocation endpoint. 48 | """ 49 | try: 50 | form_data = await request.form() 51 | revocation_request = RevocationRequest.model_validate(dict(form_data)) 52 | except ValidationError as e: 53 | return PydanticJSONResponse( 54 | status_code=400, 55 | content=RevocationErrorResponse( 56 | error="invalid_request", 57 | error_description=stringify_pydantic_error(e), 58 | ), 59 | ) 60 | 61 | # Authenticate client 62 | try: 63 | client = await self.client_authenticator.authenticate( 64 | revocation_request.client_id, revocation_request.client_secret 65 | ) 66 | except AuthenticationError as e: 67 | return PydanticJSONResponse( 68 | status_code=401, 69 | content=RevocationErrorResponse( 70 | error="unauthorized_client", 71 | error_description=e.message, 72 | ), 73 | ) 74 | 75 | loaders = [ 76 | self.provider.load_access_token, 77 | partial(self.provider.load_refresh_token, client), 78 | ] 79 | if revocation_request.token_type_hint == "refresh_token": 80 | loaders = reversed(loaders) 81 | 82 | token: None | AccessToken | RefreshToken = None 83 | for loader in loaders: 84 | token = await loader(revocation_request.token) 85 | if token is not None: 86 | break 87 | 88 | # if token is not found, just return HTTP 200 per the RFC 89 | if token and token.client_id == client.client_id: 90 | # Revoke token; provider is not meant to be able to do validation 91 | # at this point that would result in an error 92 | await self.provider.revoke_token(token) 93 | 94 | # Return successful empty response 95 | return Response( 96 | status_code=200, 97 | headers={ 98 | "Cache-Control": "no-store", 99 | "Pragma": "no-cache", 100 | }, 101 | ) 102 | -------------------------------------------------------------------------------- /src/mcp/server/auth/json_response.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from starlette.responses import JSONResponse 4 | 5 | 6 | class PydanticJSONResponse(JSONResponse): 7 | # use pydantic json serialization instead of the stock `json.dumps`, 8 | # so that we can handle serializing pydantic models like AnyHttpUrl 9 | def render(self, content: Any) -> bytes: 10 | return content.model_dump_json(exclude_none=True).encode("utf-8") 11 | -------------------------------------------------------------------------------- /src/mcp/server/auth/middleware/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Middleware for MCP authorization. 3 | """ 4 | -------------------------------------------------------------------------------- /src/mcp/server/auth/middleware/auth_context.py: -------------------------------------------------------------------------------- 1 | import contextvars 2 | 3 | from starlette.types import ASGIApp, Receive, Scope, Send 4 | 5 | from mcp.server.auth.middleware.bearer_auth import AuthenticatedUser 6 | from mcp.server.auth.provider import AccessToken 7 | 8 | # Create a contextvar to store the authenticated user 9 | # The default is None, indicating no authenticated user is present 10 | auth_context_var = contextvars.ContextVar[AuthenticatedUser | None]( 11 | "auth_context", default=None 12 | ) 13 | 14 | 15 | def get_access_token() -> AccessToken | None: 16 | """ 17 | Get the access token from the current context. 18 | 19 | Returns: 20 | The access token if an authenticated user is available, None otherwise. 21 | """ 22 | auth_user = auth_context_var.get() 23 | return auth_user.access_token if auth_user else None 24 | 25 | 26 | class AuthContextMiddleware: 27 | """ 28 | Middleware that extracts the authenticated user from the request 29 | and sets it in a contextvar for easy access throughout the request lifecycle. 30 | 31 | This middleware should be added after the AuthenticationMiddleware in the 32 | middleware stack to ensure that the user is properly authenticated before 33 | being stored in the context. 34 | """ 35 | 36 | def __init__(self, app: ASGIApp): 37 | self.app = app 38 | 39 | async def __call__(self, scope: Scope, receive: Receive, send: Send): 40 | user = scope.get("user") 41 | if isinstance(user, AuthenticatedUser): 42 | # Set the authenticated user in the contextvar 43 | token = auth_context_var.set(user) 44 | try: 45 | await self.app(scope, receive, send) 46 | finally: 47 | auth_context_var.reset(token) 48 | else: 49 | # No authenticated user, just process the request 50 | await self.app(scope, receive, send) 51 | -------------------------------------------------------------------------------- /src/mcp/server/auth/middleware/bearer_auth.py: -------------------------------------------------------------------------------- 1 | import time 2 | from typing import Any 3 | 4 | from starlette.authentication import ( 5 | AuthCredentials, 6 | AuthenticationBackend, 7 | SimpleUser, 8 | ) 9 | from starlette.exceptions import HTTPException 10 | from starlette.requests import HTTPConnection 11 | from starlette.types import Receive, Scope, Send 12 | 13 | from mcp.server.auth.provider import AccessToken, OAuthAuthorizationServerProvider 14 | 15 | 16 | class AuthenticatedUser(SimpleUser): 17 | """User with authentication info.""" 18 | 19 | def __init__(self, auth_info: AccessToken): 20 | super().__init__(auth_info.client_id) 21 | self.access_token = auth_info 22 | self.scopes = auth_info.scopes 23 | 24 | 25 | class BearerAuthBackend(AuthenticationBackend): 26 | """ 27 | Authentication backend that validates Bearer tokens. 28 | """ 29 | 30 | def __init__( 31 | self, 32 | provider: OAuthAuthorizationServerProvider[Any, Any, Any], 33 | ): 34 | self.provider = provider 35 | 36 | async def authenticate(self, conn: HTTPConnection): 37 | auth_header = next( 38 | ( 39 | conn.headers.get(key) 40 | for key in conn.headers 41 | if key.lower() == "authorization" 42 | ), 43 | None, 44 | ) 45 | if not auth_header or not auth_header.lower().startswith("bearer "): 46 | return None 47 | 48 | token = auth_header[7:] # Remove "Bearer " prefix 49 | 50 | # Validate the token with the provider 51 | auth_info = await self.provider.load_access_token(token) 52 | 53 | if not auth_info: 54 | return None 55 | 56 | if auth_info.expires_at and auth_info.expires_at < int(time.time()): 57 | return None 58 | 59 | return AuthCredentials(auth_info.scopes), AuthenticatedUser(auth_info) 60 | 61 | 62 | class RequireAuthMiddleware: 63 | """ 64 | Middleware that requires a valid Bearer token in the Authorization header. 65 | 66 | This will validate the token with the auth provider and store the resulting 67 | auth info in the request state. 68 | """ 69 | 70 | def __init__(self, app: Any, required_scopes: list[str]): 71 | """ 72 | Initialize the middleware. 73 | 74 | Args: 75 | app: ASGI application 76 | provider: Authentication provider to validate tokens 77 | required_scopes: Optional list of scopes that the token must have 78 | """ 79 | self.app = app 80 | self.required_scopes = required_scopes 81 | 82 | async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: 83 | auth_user = scope.get("user") 84 | if not isinstance(auth_user, AuthenticatedUser): 85 | raise HTTPException(status_code=401, detail="Unauthorized") 86 | auth_credentials = scope.get("auth") 87 | 88 | for required_scope in self.required_scopes: 89 | # auth_credentials should always be provided; this is just paranoia 90 | if ( 91 | auth_credentials is None 92 | or required_scope not in auth_credentials.scopes 93 | ): 94 | raise HTTPException(status_code=403, detail="Insufficient scope") 95 | 96 | await self.app(scope, receive, send) 97 | -------------------------------------------------------------------------------- /src/mcp/server/auth/middleware/client_auth.py: -------------------------------------------------------------------------------- 1 | import time 2 | from typing import Any 3 | 4 | from mcp.server.auth.provider import OAuthAuthorizationServerProvider 5 | from mcp.shared.auth import OAuthClientInformationFull 6 | 7 | 8 | class AuthenticationError(Exception): 9 | def __init__(self, message: str): 10 | self.message = message 11 | 12 | 13 | class ClientAuthenticator: 14 | """ 15 | ClientAuthenticator is a callable which validates requests from a client 16 | application, used to verify /token calls. 17 | If, during registration, the client requested to be issued a secret, the 18 | authenticator asserts that /token calls must be authenticated with 19 | that same token. 20 | NOTE: clients can opt for no authentication during registration, in which case this 21 | logic is skipped. 22 | """ 23 | 24 | def __init__(self, provider: OAuthAuthorizationServerProvider[Any, Any, Any]): 25 | """ 26 | Initialize the dependency. 27 | 28 | Args: 29 | provider: Provider to look up client information 30 | """ 31 | self.provider = provider 32 | 33 | async def authenticate( 34 | self, client_id: str, client_secret: str | None 35 | ) -> OAuthClientInformationFull: 36 | # Look up client information 37 | client = await self.provider.get_client(client_id) 38 | if not client: 39 | raise AuthenticationError("Invalid client_id") 40 | 41 | # If client from the store expects a secret, validate that the request provides 42 | # that secret 43 | if client.client_secret: 44 | if not client_secret: 45 | raise AuthenticationError("Client secret is required") 46 | 47 | if client.client_secret != client_secret: 48 | raise AuthenticationError("Invalid client_secret") 49 | 50 | if ( 51 | client.client_secret_expires_at 52 | and client.client_secret_expires_at < int(time.time()) 53 | ): 54 | raise AuthenticationError("Client secret has expired") 55 | 56 | return client 57 | -------------------------------------------------------------------------------- /src/mcp/server/auth/settings.py: -------------------------------------------------------------------------------- 1 | from pydantic import AnyHttpUrl, BaseModel, Field 2 | 3 | 4 | class ClientRegistrationOptions(BaseModel): 5 | enabled: bool = False 6 | client_secret_expiry_seconds: int | None = None 7 | valid_scopes: list[str] | None = None 8 | default_scopes: list[str] | None = None 9 | 10 | 11 | class RevocationOptions(BaseModel): 12 | enabled: bool = False 13 | 14 | 15 | class AuthSettings(BaseModel): 16 | issuer_url: AnyHttpUrl = Field( 17 | ..., 18 | description="URL advertised as OAuth issuer; this should be the URL the server " 19 | "is reachable at", 20 | ) 21 | service_documentation_url: AnyHttpUrl | None = None 22 | client_registration_options: ClientRegistrationOptions | None = None 23 | revocation_options: RevocationOptions | None = None 24 | required_scopes: list[str] | None = None 25 | -------------------------------------------------------------------------------- /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 functools 4 | import inspect 5 | from collections.abc import Callable 6 | from typing import TYPE_CHECKING, Any, get_origin 7 | 8 | from pydantic import BaseModel, Field 9 | 10 | from mcp.server.fastmcp.exceptions import ToolError 11 | from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata, func_metadata 12 | from mcp.types import ToolAnnotations 13 | 14 | if TYPE_CHECKING: 15 | from mcp.server.fastmcp.server import Context 16 | from mcp.server.session import ServerSessionT 17 | from mcp.shared.context import LifespanContextT, RequestT 18 | 19 | 20 | class Tool(BaseModel): 21 | """Internal tool registration info.""" 22 | 23 | fn: Callable[..., Any] = Field(exclude=True) 24 | name: str = Field(description="Name of the tool") 25 | description: str = Field(description="Description of what the tool does") 26 | parameters: dict[str, Any] = Field(description="JSON schema for tool parameters") 27 | fn_metadata: FuncMetadata = Field( 28 | description="Metadata about the function including a pydantic model for tool" 29 | " arguments" 30 | ) 31 | is_async: bool = Field(description="Whether the tool is async") 32 | context_kwarg: str | None = Field( 33 | None, description="Name of the kwarg that should receive context" 34 | ) 35 | annotations: ToolAnnotations | None = Field( 36 | None, description="Optional annotations for the tool" 37 | ) 38 | 39 | @classmethod 40 | def from_function( 41 | cls, 42 | fn: Callable[..., Any], 43 | name: str | None = None, 44 | description: str | None = None, 45 | context_kwarg: str | None = None, 46 | annotations: ToolAnnotations | None = None, 47 | ) -> Tool: 48 | """Create a Tool from a function.""" 49 | from mcp.server.fastmcp.server import Context 50 | 51 | func_name = name or fn.__name__ 52 | 53 | if func_name == "": 54 | raise ValueError("You must provide a name for lambda functions") 55 | 56 | func_doc = description or fn.__doc__ or "" 57 | is_async = _is_async_callable(fn) 58 | 59 | if context_kwarg is None: 60 | sig = inspect.signature(fn) 61 | for param_name, param in sig.parameters.items(): 62 | if get_origin(param.annotation) is not None: 63 | continue 64 | if issubclass(param.annotation, Context): 65 | context_kwarg = param_name 66 | break 67 | 68 | func_arg_metadata = func_metadata( 69 | fn, 70 | skip_names=[context_kwarg] if context_kwarg is not None else [], 71 | ) 72 | parameters = func_arg_metadata.arg_model.model_json_schema() 73 | 74 | return cls( 75 | fn=fn, 76 | name=func_name, 77 | description=func_doc, 78 | parameters=parameters, 79 | fn_metadata=func_arg_metadata, 80 | is_async=is_async, 81 | context_kwarg=context_kwarg, 82 | annotations=annotations, 83 | ) 84 | 85 | async def run( 86 | self, 87 | arguments: dict[str, Any], 88 | context: Context[ServerSessionT, LifespanContextT, RequestT] | None = None, 89 | ) -> Any: 90 | """Run the tool with arguments.""" 91 | try: 92 | return await self.fn_metadata.call_fn_with_arg_validation( 93 | self.fn, 94 | self.is_async, 95 | arguments, 96 | {self.context_kwarg: context} 97 | if self.context_kwarg is not None 98 | else None, 99 | ) 100 | except Exception as e: 101 | raise ToolError(f"Error executing tool {self.name}: {e}") from e 102 | 103 | 104 | def _is_async_callable(obj: Any) -> bool: 105 | while isinstance(obj, functools.partial): 106 | obj = obj.func 107 | 108 | return inspect.iscoroutinefunction(obj) or ( 109 | callable(obj) and inspect.iscoroutinefunction(getattr(obj, "__call__", None)) 110 | ) 111 | -------------------------------------------------------------------------------- /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, RequestT 10 | from mcp.types import ToolAnnotations 11 | 12 | if TYPE_CHECKING: 13 | from mcp.server.fastmcp.server import Context 14 | from mcp.server.session import ServerSessionT 15 | 16 | logger = get_logger(__name__) 17 | 18 | 19 | class ToolManager: 20 | """Manages FastMCP tools.""" 21 | 22 | def __init__( 23 | self, 24 | warn_on_duplicate_tools: bool = True, 25 | *, 26 | tools: list[Tool] | None = None, 27 | ): 28 | self._tools: dict[str, Tool] = {} 29 | if tools is not None: 30 | for tool in tools: 31 | if warn_on_duplicate_tools and tool.name in self._tools: 32 | logger.warning(f"Tool already exists: {tool.name}") 33 | self._tools[tool.name] = tool 34 | 35 | self.warn_on_duplicate_tools = warn_on_duplicate_tools 36 | 37 | def get_tool(self, name: str) -> Tool | None: 38 | """Get tool by name.""" 39 | return self._tools.get(name) 40 | 41 | def list_tools(self) -> list[Tool]: 42 | """List all registered tools.""" 43 | return list(self._tools.values()) 44 | 45 | def add_tool( 46 | self, 47 | fn: Callable[..., Any], 48 | name: str | None = None, 49 | description: str | None = None, 50 | annotations: ToolAnnotations | None = None, 51 | ) -> Tool: 52 | """Add a tool to the server.""" 53 | tool = Tool.from_function( 54 | fn, name=name, description=description, annotations=annotations 55 | ) 56 | existing = self._tools.get(tool.name) 57 | if existing: 58 | if self.warn_on_duplicate_tools: 59 | logger.warning(f"Tool already exists: {tool.name}") 60 | return existing 61 | self._tools[tool.name] = tool 62 | return tool 63 | 64 | async def call_tool( 65 | self, 66 | name: str, 67 | arguments: dict[str, Any], 68 | context: Context[ServerSessionT, LifespanContextT, RequestT] | None = None, 69 | ) -> Any: 70 | """Call a tool by name with arguments.""" 71 | tool = self.get_tool(name) 72 | if not tool: 73 | raise ToolError(f"Unknown tool: {name}") 74 | 75 | return await tool.run(arguments, context=context) 76 | -------------------------------------------------------------------------------- /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 | from mcp.shared.message import SessionMessage 31 | 32 | 33 | @asynccontextmanager 34 | async def stdio_server( 35 | stdin: anyio.AsyncFile[str] | None = None, 36 | stdout: anyio.AsyncFile[str] | None = None, 37 | ): 38 | """ 39 | Server transport for stdio: this communicates with an MCP client by reading 40 | from the current process' stdin and writing to stdout. 41 | """ 42 | # Purposely not using context managers for these, as we don't want to close 43 | # standard process handles. Encoding of stdin/stdout as text streams on 44 | # python is platform-dependent (Windows is particularly problematic), so we 45 | # re-wrap the underlying binary stream to ensure UTF-8. 46 | if not stdin: 47 | stdin = anyio.wrap_file(TextIOWrapper(sys.stdin.buffer, encoding="utf-8")) 48 | if not stdout: 49 | stdout = anyio.wrap_file(TextIOWrapper(sys.stdout.buffer, encoding="utf-8")) 50 | 51 | read_stream: MemoryObjectReceiveStream[SessionMessage | Exception] 52 | read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception] 53 | 54 | write_stream: MemoryObjectSendStream[SessionMessage] 55 | write_stream_reader: MemoryObjectReceiveStream[SessionMessage] 56 | 57 | read_stream_writer, read_stream = anyio.create_memory_object_stream(0) 58 | write_stream, write_stream_reader = anyio.create_memory_object_stream(0) 59 | 60 | async def stdin_reader(): 61 | try: 62 | async with read_stream_writer: 63 | async for line in stdin: 64 | try: 65 | message = types.JSONRPCMessage.model_validate_json(line) 66 | except Exception as exc: 67 | await read_stream_writer.send(exc) 68 | continue 69 | 70 | session_message = SessionMessage(message) 71 | await read_stream_writer.send(session_message) 72 | except anyio.ClosedResourceError: 73 | await anyio.lowlevel.checkpoint() 74 | 75 | async def stdout_writer(): 76 | try: 77 | async with write_stream_reader: 78 | async for session_message in write_stream_reader: 79 | json = session_message.message.model_dump_json( 80 | by_alias=True, exclude_none=True 81 | ) 82 | await stdout.write(json + "\n") 83 | await stdout.flush() 84 | except anyio.ClosedResourceError: 85 | await anyio.lowlevel.checkpoint() 86 | 87 | async with anyio.create_task_group() as tg: 88 | tg.start_soon(stdin_reader) 89 | tg.start_soon(stdout_writer) 90 | yield read_stream, write_stream 91 | -------------------------------------------------------------------------------- /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 | from mcp.shared.message import SessionMessage 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | @asynccontextmanager 17 | async def websocket_server(scope: Scope, receive: Receive, send: Send): 18 | """ 19 | WebSocket server transport for MCP. This is an ASGI application, suitable to be 20 | used with a framework like Starlette and a server like Hypercorn. 21 | """ 22 | 23 | websocket = WebSocket(scope, receive, send) 24 | await websocket.accept(subprotocol="mcp") 25 | 26 | read_stream: MemoryObjectReceiveStream[SessionMessage | Exception] 27 | read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception] 28 | 29 | write_stream: MemoryObjectSendStream[SessionMessage] 30 | write_stream_reader: MemoryObjectReceiveStream[SessionMessage] 31 | 32 | read_stream_writer, read_stream = anyio.create_memory_object_stream(0) 33 | write_stream, write_stream_reader = anyio.create_memory_object_stream(0) 34 | 35 | async def ws_reader(): 36 | try: 37 | async with read_stream_writer: 38 | async for msg in websocket.iter_text(): 39 | try: 40 | client_message = types.JSONRPCMessage.model_validate_json(msg) 41 | except ValidationError as exc: 42 | await read_stream_writer.send(exc) 43 | continue 44 | 45 | session_message = SessionMessage(client_message) 46 | await read_stream_writer.send(session_message) 47 | except anyio.ClosedResourceError: 48 | await websocket.close() 49 | 50 | async def ws_writer(): 51 | try: 52 | async with write_stream_reader: 53 | async for session_message in write_stream_reader: 54 | obj = session_message.message.model_dump_json( 55 | by_alias=True, exclude_none=True 56 | ) 57 | await websocket.send_text(obj) 58 | except anyio.ClosedResourceError: 59 | await websocket.close() 60 | 61 | async with anyio.create_task_group() as tg: 62 | tg.start_soon(ws_reader) 63 | tg.start_soon(ws_writer) 64 | yield (read_stream, write_stream) 65 | -------------------------------------------------------------------------------- /src/mcp/shared/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/544176770b53e6a0ae8c413d3b6c5116421f67df/src/mcp/shared/__init__.py -------------------------------------------------------------------------------- /src/mcp/shared/_httpx_utils.py: -------------------------------------------------------------------------------- 1 | """Utilities for creating standardized httpx AsyncClient instances.""" 2 | 3 | from typing import Any, Protocol 4 | 5 | import httpx 6 | 7 | __all__ = ["create_mcp_http_client"] 8 | 9 | 10 | class McpHttpClientFactory(Protocol): 11 | def __call__( 12 | self, 13 | headers: dict[str, str] | None = None, 14 | timeout: httpx.Timeout | None = None, 15 | auth: httpx.Auth | None = None, 16 | ) -> httpx.AsyncClient: ... 17 | 18 | 19 | def create_mcp_http_client( 20 | headers: dict[str, str] | None = None, 21 | timeout: httpx.Timeout | None = None, 22 | auth: httpx.Auth | None = None, 23 | ) -> httpx.AsyncClient: 24 | """Create a standardized httpx AsyncClient with MCP defaults. 25 | 26 | This function provides common defaults used throughout the MCP codebase: 27 | - follow_redirects=True (always enabled) 28 | - Default timeout of 30 seconds if not specified 29 | 30 | Args: 31 | headers: Optional headers to include with all requests. 32 | timeout: Request timeout as httpx.Timeout object. 33 | Defaults to 30 seconds if not specified. 34 | auth: Optional authentication handler. 35 | 36 | Returns: 37 | Configured httpx.AsyncClient instance with MCP defaults. 38 | 39 | Note: 40 | The returned AsyncClient must be used as a context manager to ensure 41 | proper cleanup of connections. 42 | 43 | Examples: 44 | # Basic usage with MCP defaults 45 | async with create_mcp_http_client() as client: 46 | response = await client.get("https://api.example.com") 47 | 48 | # With custom headers 49 | headers = {"Authorization": "Bearer token"} 50 | async with create_mcp_http_client(headers) as client: 51 | response = await client.get("/endpoint") 52 | 53 | # With both custom headers and timeout 54 | timeout = httpx.Timeout(60.0, read=300.0) 55 | async with create_mcp_http_client(headers, timeout) as client: 56 | response = await client.get("/long-request") 57 | 58 | # With authentication 59 | from httpx import BasicAuth 60 | auth = BasicAuth(username="user", password="pass") 61 | async with create_mcp_http_client(headers, timeout, auth) as client: 62 | response = await client.get("/protected-endpoint") 63 | """ 64 | # Set MCP defaults 65 | kwargs: dict[str, Any] = { 66 | "follow_redirects": True, 67 | } 68 | 69 | # Handle timeout 70 | if timeout is None: 71 | kwargs["timeout"] = httpx.Timeout(30.0) 72 | else: 73 | kwargs["timeout"] = timeout 74 | 75 | # Handle headers 76 | if headers is not None: 77 | kwargs["headers"] = headers 78 | 79 | # Handle authentication 80 | if auth is not None: 81 | kwargs["auth"] = auth 82 | 83 | return httpx.AsyncClient(**kwargs) 84 | -------------------------------------------------------------------------------- /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 | RequestT = TypeVar("RequestT", default=Any) 12 | 13 | 14 | @dataclass 15 | class RequestContext(Generic[SessionT, LifespanContextT, RequestT]): 16 | request_id: RequestId 17 | meta: RequestParams.Meta | None 18 | session: SessionT 19 | lifespan_context: LifespanContextT 20 | request: RequestT | None = None 21 | -------------------------------------------------------------------------------- /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.shared.message import SessionMessage 23 | 24 | MessageStream = tuple[ 25 | MemoryObjectReceiveStream[SessionMessage | Exception], 26 | MemoryObjectSendStream[SessionMessage], 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 | SessionMessage | Exception 44 | ](1) 45 | client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[ 46 | SessionMessage | 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/message.py: -------------------------------------------------------------------------------- 1 | """ 2 | Message wrapper with metadata support. 3 | 4 | This module defines a wrapper type that combines JSONRPCMessage with metadata 5 | to support transport-specific features like resumability. 6 | """ 7 | 8 | from collections.abc import Awaitable, Callable 9 | from dataclasses import dataclass 10 | 11 | from mcp.types import JSONRPCMessage, RequestId 12 | 13 | ResumptionToken = str 14 | 15 | ResumptionTokenUpdateCallback = Callable[[ResumptionToken], Awaitable[None]] 16 | 17 | 18 | @dataclass 19 | class ClientMessageMetadata: 20 | """Metadata specific to client messages.""" 21 | 22 | resumption_token: ResumptionToken | None = None 23 | on_resumption_token_update: Callable[[ResumptionToken], Awaitable[None]] | None = ( 24 | None 25 | ) 26 | 27 | 28 | @dataclass 29 | class ServerMessageMetadata: 30 | """Metadata specific to server messages.""" 31 | 32 | related_request_id: RequestId | None = None 33 | # Request-specific context (e.g., headers, auth info) 34 | request_context: object | None = None 35 | 36 | 37 | MessageMetadata = ClientMessageMetadata | ServerMessageMetadata | None 38 | 39 | 40 | @dataclass 41 | class SessionMessage: 42 | """A message with specific metadata for transport-specific features.""" 43 | 44 | message: JSONRPCMessage 45 | metadata: MessageMetadata = None 46 | -------------------------------------------------------------------------------- /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, message: str | None = None) -> None: 47 | self.current += amount 48 | 49 | await self.session.send_progress_notification( 50 | self.progress_token, self.current, total=self.total, message=message 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: list[str] = ["2024-11-05", LATEST_PROTOCOL_VERSION] 4 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/544176770b53e6a0ae8c413d3b6c5116421f67df/tests/__init__.py -------------------------------------------------------------------------------- /tests/client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/544176770b53e6a0ae8c413d3b6c5116421f67df/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( 48 | test_args, capture_output=True, text=True, timeout=5, check=False 49 | ) 50 | 51 | assert result.returncode == 0 52 | assert "usage" in result.stdout.lower() 53 | 54 | 55 | def test_absolute_uv_path(mock_config_path: Path): 56 | """Test that the absolute path to uv is used when available.""" 57 | # Mock the shutil.which function to return a fake path 58 | mock_uv_path = "/usr/local/bin/uv" 59 | 60 | with patch("mcp.cli.claude.get_uv_path", return_value=mock_uv_path): 61 | # Setup 62 | server_name = "test_server" 63 | file_spec = "test_server.py:app" 64 | 65 | # Update config 66 | success = update_claude_config(file_spec=file_spec, server_name=server_name) 67 | assert success 68 | 69 | # Read the generated config 70 | config_file = mock_config_path / "claude_desktop_config.json" 71 | config = json.loads(config_file.read_text()) 72 | 73 | # Verify the command is the absolute path 74 | server_config = config["mcpServers"][server_name] 75 | command = server_config["command"] 76 | 77 | assert command == mock_uv_path 78 | -------------------------------------------------------------------------------- /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 | # Create meta object with related_request_id added dynamically 82 | log = logging_collector.log_messages[0] 83 | assert log.level == "info" 84 | assert log.logger == "test_logger" 85 | assert log.data == "Test log message" 86 | -------------------------------------------------------------------------------- /tests/client/test_resource_cleanup.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | import anyio 4 | import pytest 5 | 6 | from mcp.shared.session import BaseSession 7 | from mcp.types import ( 8 | ClientRequest, 9 | EmptyResult, 10 | PingRequest, 11 | ) 12 | 13 | 14 | @pytest.mark.anyio 15 | async def test_send_request_stream_cleanup(): 16 | """ 17 | Test that send_request properly cleans up streams when an exception occurs. 18 | 19 | This test mocks out most of the session functionality to focus on stream cleanup. 20 | """ 21 | 22 | # Create a mock session with the minimal required functionality 23 | class TestSession(BaseSession): 24 | async def _send_response(self, request_id, response): 25 | pass 26 | 27 | # Create streams 28 | write_stream_send, write_stream_receive = anyio.create_memory_object_stream(1) 29 | read_stream_send, read_stream_receive = anyio.create_memory_object_stream(1) 30 | 31 | # Create the session 32 | session = TestSession( 33 | read_stream_receive, 34 | write_stream_send, 35 | object, # Request type doesn't matter for this test 36 | object, # Notification type doesn't matter for this test 37 | ) 38 | 39 | # Create a test request 40 | request = ClientRequest( 41 | PingRequest( 42 | method="ping", 43 | ) 44 | ) 45 | 46 | # Patch the _write_stream.send method to raise an exception 47 | async def mock_send(*args, **kwargs): 48 | raise RuntimeError("Simulated network error") 49 | 50 | # Record the response streams before the test 51 | initial_stream_count = len(session._response_streams) 52 | 53 | # Run the test with the patched method 54 | with patch.object(session._write_stream, "send", mock_send): 55 | with pytest.raises(RuntimeError): 56 | await session.send_request(request, EmptyResult) 57 | 58 | # Verify that no response streams were leaked 59 | assert len(session._response_streams) == initial_stream_count, ( 60 | f"Expected {initial_stream_count} response streams after request, " 61 | f"but found {len(session._response_streams)}" 62 | ) 63 | 64 | # Clean up 65 | await write_stream_send.aclose() 66 | await write_stream_receive.aclose() 67 | await read_stream_send.aclose() 68 | await read_stream_receive.aclose() 69 | -------------------------------------------------------------------------------- /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.session import ClientSession 6 | from mcp.client.stdio import ( 7 | StdioServerParameters, 8 | stdio_client, 9 | ) 10 | from mcp.shared.exceptions import McpError 11 | from mcp.shared.message import SessionMessage 12 | from mcp.types import CONNECTION_CLOSED, JSONRPCMessage, JSONRPCRequest, JSONRPCResponse 13 | 14 | tee: str = shutil.which("tee") # type: ignore 15 | python: str = shutil.which("python") # type: ignore 16 | 17 | 18 | @pytest.mark.anyio 19 | @pytest.mark.skipif(tee is None, reason="could not find tee command") 20 | async def test_stdio_context_manager_exiting(): 21 | async with stdio_client(StdioServerParameters(command=tee)) as (_, _): 22 | pass 23 | 24 | 25 | @pytest.mark.anyio 26 | @pytest.mark.skipif(tee is None, reason="could not find tee command") 27 | async def test_stdio_client(): 28 | server_parameters = StdioServerParameters(command=tee) 29 | 30 | async with stdio_client(server_parameters) as (read_stream, write_stream): 31 | # Test sending and receiving messages 32 | messages = [ 33 | JSONRPCMessage(root=JSONRPCRequest(jsonrpc="2.0", id=1, method="ping")), 34 | JSONRPCMessage(root=JSONRPCResponse(jsonrpc="2.0", id=2, result={})), 35 | ] 36 | 37 | async with write_stream: 38 | for message in messages: 39 | session_message = SessionMessage(message) 40 | await write_stream.send(session_message) 41 | 42 | read_messages = [] 43 | async with read_stream: 44 | async for message in read_stream: 45 | if isinstance(message, Exception): 46 | raise message 47 | 48 | read_messages.append(message.message) 49 | if len(read_messages) == 2: 50 | break 51 | 52 | assert len(read_messages) == 2 53 | assert read_messages[0] == JSONRPCMessage( 54 | root=JSONRPCRequest(jsonrpc="2.0", id=1, method="ping") 55 | ) 56 | assert read_messages[1] == JSONRPCMessage( 57 | root=JSONRPCResponse(jsonrpc="2.0", id=2, result={}) 58 | ) 59 | 60 | 61 | @pytest.mark.anyio 62 | async def test_stdio_client_bad_path(): 63 | """Check that the connection doesn't hang if process errors.""" 64 | server_params = StdioServerParameters( 65 | command="python", args=["-c", "non-existent-file.py"] 66 | ) 67 | async with stdio_client(server_params) as (read_stream, write_stream): 68 | async with ClientSession(read_stream, write_stream) as session: 69 | # The session should raise an error when the connection closes 70 | with pytest.raises(McpError) as exc_info: 71 | await session.initialize() 72 | 73 | # Check that we got a connection closed error 74 | assert exc_info.value.error.code == CONNECTION_CLOSED 75 | assert "Connection closed" in exc_info.value.error.message 76 | 77 | 78 | @pytest.mark.anyio 79 | async def test_stdio_client_nonexistent_command(): 80 | """Test that stdio_client raises an error for non-existent commands.""" 81 | # Create a server with a non-existent command 82 | server_params = StdioServerParameters( 83 | command="/path/to/nonexistent/command", 84 | args=["--help"], 85 | ) 86 | 87 | # Should raise an error when trying to start the process 88 | with pytest.raises(Exception) as exc_info: 89 | async with stdio_client(server_params) as (_, _): 90 | pass 91 | 92 | # The error should indicate the command was not found 93 | error_message = str(exc_info.value) 94 | assert ( 95 | "nonexistent" in error_message 96 | or "not found" in error_message.lower() 97 | or "cannot find the file" in error_message.lower() # Windows error message 98 | ) 99 | -------------------------------------------------------------------------------- /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 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_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, message=None 43 | ) 44 | mock_session.send_progress_notification.assert_any_call( 45 | progress_token=0, progress=5.0, total=10.0, message=None 46 | ) 47 | mock_session.send_progress_notification.assert_any_call( 48 | progress_token=0, progress=10.0, total=10.0, message=None 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 < 6 * _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.shared.message import SessionMessage 7 | from mcp.types import ( 8 | LATEST_PROTOCOL_VERSION, 9 | ClientCapabilities, 10 | Implementation, 11 | InitializeRequestParams, 12 | JSONRPCMessage, 13 | JSONRPCNotification, 14 | JSONRPCRequest, 15 | NotificationParams, 16 | ) 17 | 18 | 19 | @pytest.mark.anyio 20 | async def test_request_id_match() -> None: 21 | """Test that the server preserves request IDs in responses.""" 22 | server = Server("test") 23 | custom_request_id = "test-123" 24 | 25 | # Create memory streams for communication 26 | client_writer, client_reader = anyio.create_memory_object_stream(1) 27 | server_writer, server_reader = anyio.create_memory_object_stream(1) 28 | 29 | # Server task to process the request 30 | async def run_server(): 31 | async with client_reader, server_writer: 32 | await server.run( 33 | client_reader, 34 | server_writer, 35 | InitializationOptions( 36 | server_name="test", 37 | server_version="1.0.0", 38 | capabilities=server.get_capabilities( 39 | notification_options=NotificationOptions(), 40 | experimental_capabilities={}, 41 | ), 42 | ), 43 | raise_exceptions=True, 44 | ) 45 | 46 | # Start server task 47 | async with ( 48 | anyio.create_task_group() as tg, 49 | client_writer, 50 | client_reader, 51 | server_writer, 52 | server_reader, 53 | ): 54 | tg.start_soon(run_server) 55 | 56 | # Send initialize request 57 | init_req = JSONRPCRequest( 58 | id="init-1", 59 | method="initialize", 60 | params=InitializeRequestParams( 61 | protocolVersion=LATEST_PROTOCOL_VERSION, 62 | capabilities=ClientCapabilities(), 63 | clientInfo=Implementation(name="test-client", version="1.0.0"), 64 | ).model_dump(by_alias=True, exclude_none=True), 65 | jsonrpc="2.0", 66 | ) 67 | 68 | await client_writer.send(SessionMessage(JSONRPCMessage(root=init_req))) 69 | response = ( 70 | await server_reader.receive() 71 | ) # Get init response but don't need to check it 72 | 73 | # Send initialized notification 74 | initialized_notification = JSONRPCNotification( 75 | method="notifications/initialized", 76 | params=NotificationParams().model_dump(by_alias=True, exclude_none=True), 77 | jsonrpc="2.0", 78 | ) 79 | await client_writer.send( 80 | SessionMessage(JSONRPCMessage(root=initialized_notification)) 81 | ) 82 | 83 | # Send ping request with custom ID 84 | ping_request = JSONRPCRequest( 85 | id=custom_request_id, method="ping", params={}, jsonrpc="2.0" 86 | ) 87 | 88 | await client_writer.send(SessionMessage(JSONRPCMessage(root=ping_request))) 89 | 90 | # Read response 91 | response = await server_reader.receive() 92 | 93 | # Verify response ID matches request ID 94 | assert ( 95 | response.message.root.id == custom_request_id 96 | ), "Response ID should match request ID" 97 | 98 | # Cancel server task 99 | tg.cancel_scope.cancel() 100 | -------------------------------------------------------------------------------- /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/server/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/544176770b53e6a0ae8c413d3b6c5116421f67df/tests/server/__init__.py -------------------------------------------------------------------------------- /tests/server/fastmcp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/544176770b53e6a0ae8c413d3b6c5116421f67df/tests/server/fastmcp/__init__.py -------------------------------------------------------------------------------- /tests/server/fastmcp/auth/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for the MCP server auth components. 3 | """ 4 | -------------------------------------------------------------------------------- /tests/server/fastmcp/prompts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/544176770b53e6a0ae8c413d3b6c5116421f67df/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/544176770b53e6a0ae8c413d3b6c5116421f67df/tests/server/fastmcp/resources/__init__.py -------------------------------------------------------------------------------- /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/544176770b53e6a0ae8c413d3b6c5116421f67df/tests/server/fastmcp/servers/__init__.py -------------------------------------------------------------------------------- /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_lowlevel_tool_annotations.py: -------------------------------------------------------------------------------- 1 | """Tests for tool annotations in low-level server.""" 2 | 3 | import anyio 4 | import pytest 5 | 6 | from mcp.client.session import ClientSession 7 | from mcp.server import Server 8 | from mcp.server.lowlevel import NotificationOptions 9 | from mcp.server.models import InitializationOptions 10 | from mcp.server.session import ServerSession 11 | from mcp.shared.message import SessionMessage 12 | from mcp.shared.session import RequestResponder 13 | from mcp.types import ( 14 | ClientResult, 15 | ServerNotification, 16 | ServerRequest, 17 | Tool, 18 | ToolAnnotations, 19 | ) 20 | 21 | 22 | @pytest.mark.anyio 23 | async def test_lowlevel_server_tool_annotations(): 24 | """Test that tool annotations work in low-level server.""" 25 | server = Server("test") 26 | 27 | # Create a tool with annotations 28 | @server.list_tools() 29 | async def list_tools(): 30 | return [ 31 | Tool( 32 | name="echo", 33 | description="Echo a message back", 34 | inputSchema={ 35 | "type": "object", 36 | "properties": { 37 | "message": {"type": "string"}, 38 | }, 39 | "required": ["message"], 40 | }, 41 | annotations=ToolAnnotations( 42 | title="Echo Tool", 43 | readOnlyHint=True, 44 | ), 45 | ) 46 | ] 47 | 48 | server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[ 49 | SessionMessage 50 | ](10) 51 | client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[ 52 | SessionMessage 53 | ](10) 54 | 55 | # Message handler for client 56 | async def message_handler( 57 | message: RequestResponder[ServerRequest, ClientResult] 58 | | ServerNotification 59 | | Exception, 60 | ) -> None: 61 | if isinstance(message, Exception): 62 | raise message 63 | 64 | # Server task 65 | async def run_server(): 66 | async with ServerSession( 67 | client_to_server_receive, 68 | server_to_client_send, 69 | InitializationOptions( 70 | server_name="test-server", 71 | server_version="1.0.0", 72 | capabilities=server.get_capabilities( 73 | notification_options=NotificationOptions(), 74 | experimental_capabilities={}, 75 | ), 76 | ), 77 | ) as server_session: 78 | async with anyio.create_task_group() as tg: 79 | 80 | async def handle_messages(): 81 | async for message in server_session.incoming_messages: 82 | await server._handle_message(message, server_session, {}, False) 83 | 84 | tg.start_soon(handle_messages) 85 | await anyio.sleep_forever() 86 | 87 | # Run the test 88 | async with anyio.create_task_group() as tg: 89 | tg.start_soon(run_server) 90 | 91 | async with ClientSession( 92 | server_to_client_receive, 93 | client_to_server_send, 94 | message_handler=message_handler, 95 | ) as client_session: 96 | # Initialize the session 97 | await client_session.initialize() 98 | 99 | # List tools 100 | tools_result = await client_session.list_tools() 101 | 102 | # Cancel the server task 103 | tg.cancel_scope.cancel() 104 | 105 | # Verify results 106 | assert tools_result is not None 107 | assert len(tools_result.tools) == 1 108 | assert tools_result.tools[0].name == "echo" 109 | assert tools_result.tools[0].annotations is not None 110 | assert tools_result.tools[0].annotations.title == "Echo Tool" 111 | assert tools_result.tools[0].annotations.readOnlyHint is True 112 | -------------------------------------------------------------------------------- /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_stdio.py: -------------------------------------------------------------------------------- 1 | import io 2 | 3 | import anyio 4 | import pytest 5 | 6 | from mcp.server.stdio import stdio_server 7 | from mcp.shared.message import SessionMessage 8 | from mcp.types import JSONRPCMessage, JSONRPCRequest, JSONRPCResponse 9 | 10 | 11 | @pytest.mark.anyio 12 | async def test_stdio_server(): 13 | stdin = io.StringIO() 14 | stdout = io.StringIO() 15 | 16 | messages = [ 17 | JSONRPCMessage(root=JSONRPCRequest(jsonrpc="2.0", id=1, method="ping")), 18 | JSONRPCMessage(root=JSONRPCResponse(jsonrpc="2.0", id=2, result={})), 19 | ] 20 | 21 | for message in messages: 22 | stdin.write(message.model_dump_json(by_alias=True, exclude_none=True) + "\n") 23 | stdin.seek(0) 24 | 25 | async with stdio_server( 26 | stdin=anyio.AsyncFile(stdin), stdout=anyio.AsyncFile(stdout) 27 | ) as (read_stream, write_stream): 28 | received_messages = [] 29 | async with read_stream: 30 | async for message in read_stream: 31 | if isinstance(message, Exception): 32 | raise message 33 | received_messages.append(message.message) 34 | if len(received_messages) == 2: 35 | break 36 | 37 | # Verify received messages 38 | assert len(received_messages) == 2 39 | assert received_messages[0] == JSONRPCMessage( 40 | root=JSONRPCRequest(jsonrpc="2.0", id=1, method="ping") 41 | ) 42 | assert received_messages[1] == JSONRPCMessage( 43 | root=JSONRPCResponse(jsonrpc="2.0", id=2, result={}) 44 | ) 45 | 46 | # Test sending responses from the server 47 | responses = [ 48 | JSONRPCMessage(root=JSONRPCRequest(jsonrpc="2.0", id=3, method="ping")), 49 | JSONRPCMessage(root=JSONRPCResponse(jsonrpc="2.0", id=4, result={})), 50 | ] 51 | 52 | async with write_stream: 53 | for response in responses: 54 | session_message = SessionMessage(response) 55 | await write_stream.send(session_message) 56 | 57 | stdout.seek(0) 58 | output_lines = stdout.readlines() 59 | assert len(output_lines) == 2 60 | 61 | received_responses = [ 62 | JSONRPCMessage.model_validate_json(line.strip()) for line in output_lines 63 | ] 64 | assert len(received_responses) == 2 65 | assert received_responses[0] == JSONRPCMessage( 66 | root=JSONRPCRequest(jsonrpc="2.0", id=3, method="ping") 67 | ) 68 | assert received_responses[1] == JSONRPCMessage( 69 | root=JSONRPCResponse(jsonrpc="2.0", id=4, result={}) 70 | ) 71 | -------------------------------------------------------------------------------- /tests/server/test_streamable_http_manager.py: -------------------------------------------------------------------------------- 1 | """Tests for StreamableHTTPSessionManager.""" 2 | 3 | import anyio 4 | import pytest 5 | 6 | from mcp.server.lowlevel import Server 7 | from mcp.server.streamable_http_manager import StreamableHTTPSessionManager 8 | 9 | 10 | @pytest.mark.anyio 11 | async def test_run_can_only_be_called_once(): 12 | """Test that run() can only be called once per instance.""" 13 | app = Server("test-server") 14 | manager = StreamableHTTPSessionManager(app=app) 15 | 16 | # First call should succeed 17 | async with manager.run(): 18 | pass 19 | 20 | # Second call should raise RuntimeError 21 | with pytest.raises(RuntimeError) as excinfo: 22 | async with manager.run(): 23 | pass 24 | 25 | assert ( 26 | "StreamableHTTPSessionManager .run() can only be called once per instance" 27 | in str(excinfo.value) 28 | ) 29 | 30 | 31 | @pytest.mark.anyio 32 | async def test_run_prevents_concurrent_calls(): 33 | """Test that concurrent calls to run() are prevented.""" 34 | app = Server("test-server") 35 | manager = StreamableHTTPSessionManager(app=app) 36 | 37 | errors = [] 38 | 39 | async def try_run(): 40 | try: 41 | async with manager.run(): 42 | # Simulate some work 43 | await anyio.sleep(0.1) 44 | except RuntimeError as e: 45 | errors.append(e) 46 | 47 | # Try to run concurrently 48 | async with anyio.create_task_group() as tg: 49 | tg.start_soon(try_run) 50 | tg.start_soon(try_run) 51 | 52 | # One should succeed, one should fail 53 | assert len(errors) == 1 54 | assert ( 55 | "StreamableHTTPSessionManager .run() can only be called once per instance" 56 | in str(errors[0]) 57 | ) 58 | 59 | 60 | @pytest.mark.anyio 61 | async def test_handle_request_without_run_raises_error(): 62 | """Test that handle_request raises error if run() hasn't been called.""" 63 | app = Server("test-server") 64 | manager = StreamableHTTPSessionManager(app=app) 65 | 66 | # Mock ASGI parameters 67 | scope = {"type": "http", "method": "POST", "path": "/test"} 68 | 69 | async def receive(): 70 | return {"type": "http.request", "body": b""} 71 | 72 | async def send(message): 73 | pass 74 | 75 | # Should raise error because run() hasn't been called 76 | with pytest.raises(RuntimeError) as excinfo: 77 | await manager.handle_request(scope, receive, send) 78 | 79 | assert "Task group is not initialized. Make sure to use run()." in str( 80 | excinfo.value 81 | ) 82 | -------------------------------------------------------------------------------- /tests/shared/test_httpx_utils.py: -------------------------------------------------------------------------------- 1 | """Tests for httpx utility functions.""" 2 | 3 | import httpx 4 | 5 | from mcp.shared._httpx_utils import create_mcp_http_client 6 | 7 | 8 | def test_default_settings(): 9 | """Test that default settings are applied correctly.""" 10 | client = create_mcp_http_client() 11 | 12 | assert client.follow_redirects is True 13 | assert client.timeout.connect == 30.0 14 | 15 | 16 | def test_custom_parameters(): 17 | """Test custom headers and timeout are set correctly.""" 18 | headers = {"Authorization": "Bearer token"} 19 | timeout = httpx.Timeout(60.0) 20 | 21 | client = create_mcp_http_client(headers, timeout) 22 | 23 | assert client.headers["Authorization"] == "Bearer token" 24 | assert client.timeout.connect == 60.0 25 | -------------------------------------------------------------------------------- /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/test_examples.py: -------------------------------------------------------------------------------- 1 | """Tests for example servers""" 2 | 3 | import sys 4 | 5 | import pytest 6 | from pytest_examples import CodeExample, EvalExample, find_examples 7 | 8 | from mcp.shared.memory import ( 9 | create_connected_server_and_client_session as client_session, 10 | ) 11 | from mcp.types import TextContent, TextResourceContents 12 | 13 | 14 | @pytest.mark.anyio 15 | async def test_simple_echo(): 16 | """Test the simple echo server""" 17 | from examples.fastmcp.simple_echo import mcp 18 | 19 | async with client_session(mcp._mcp_server) as client: 20 | result = await client.call_tool("echo", {"text": "hello"}) 21 | assert len(result.content) == 1 22 | content = result.content[0] 23 | assert isinstance(content, TextContent) 24 | assert content.text == "hello" 25 | 26 | 27 | @pytest.mark.anyio 28 | async def test_complex_inputs(): 29 | """Test the complex inputs server""" 30 | from examples.fastmcp.complex_inputs import mcp 31 | 32 | async with client_session(mcp._mcp_server) as client: 33 | tank = {"shrimp": [{"name": "bob"}, {"name": "alice"}]} 34 | result = await client.call_tool( 35 | "name_shrimp", {"tank": tank, "extra_names": ["charlie"]} 36 | ) 37 | assert len(result.content) == 3 38 | assert isinstance(result.content[0], TextContent) 39 | assert isinstance(result.content[1], TextContent) 40 | assert isinstance(result.content[2], TextContent) 41 | assert result.content[0].text == "bob" 42 | assert result.content[1].text == "alice" 43 | assert result.content[2].text == "charlie" 44 | 45 | 46 | @pytest.mark.anyio 47 | async def test_desktop(monkeypatch): 48 | """Test the desktop server""" 49 | from pathlib import Path 50 | 51 | from pydantic import AnyUrl 52 | 53 | from examples.fastmcp.desktop import mcp 54 | 55 | # Mock desktop directory listing 56 | mock_files = [Path("/fake/path/file1.txt"), Path("/fake/path/file2.txt")] 57 | monkeypatch.setattr(Path, "iterdir", lambda self: mock_files) 58 | monkeypatch.setattr(Path, "home", lambda: Path("/fake/home")) 59 | 60 | async with client_session(mcp._mcp_server) as client: 61 | # Test the add function 62 | result = await client.call_tool("add", {"a": 1, "b": 2}) 63 | assert len(result.content) == 1 64 | content = result.content[0] 65 | assert isinstance(content, TextContent) 66 | assert content.text == "3" 67 | 68 | # Test the desktop resource 69 | result = await client.read_resource(AnyUrl("dir://desktop")) 70 | assert len(result.contents) == 1 71 | content = result.contents[0] 72 | assert isinstance(content, TextResourceContents) 73 | assert isinstance(content.text, str) 74 | if sys.platform == "win32": 75 | file_1 = "/fake/path/file1.txt".replace("/", "\\\\") # might be a bug 76 | file_2 = "/fake/path/file2.txt".replace("/", "\\\\") # might be a bug 77 | assert file_1 in content.text 78 | assert file_2 in content.text 79 | # might be a bug, but the test is passing 80 | else: 81 | assert "/fake/path/file1.txt" in content.text 82 | assert "/fake/path/file2.txt" in content.text 83 | 84 | 85 | @pytest.mark.parametrize("example", find_examples("README.md"), ids=str) 86 | def test_docs_examples(example: CodeExample, eval_example: EvalExample): 87 | ruff_ignore: list[str] = ["F841", "I001"] 88 | 89 | eval_example.set_config( 90 | ruff_ignore=ruff_ignore, target_version="py310", line_length=88 91 | ) 92 | 93 | if eval_example.update_examples: # pragma: no cover 94 | eval_example.format(example) 95 | else: 96 | eval_example.lint(example) 97 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------