| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -30,9 +30,10 @@ jobs: | |||
| 30 | 30 | env: | |
| 31 | 31 | SKIP: no-commit-to-branch,readme-v1-frozen | |
| 32 | 32 | ||
| 33 | - # TODO(Max): Drop this in v2. | ||
| 33 | + # TODO(Max): Drop this in v2. Deliberate updates (e.g. the v2 status | ||
| 34 | + # banner) go through the 'override-readme-freeze' label. | ||
| 34 | 35 | - name: Check README.md is not modified | |
| 35 | - if: github.event_name == 'pull_request' | ||
| 36 | + if: github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'override-readme-freeze') | ||
| 36 | 37 | run: | | |
| 37 | 38 | git fetch --no-tags --depth=1 origin "$BASE_SHA" | |
| 38 | 39 | if git diff --name-only "$BASE_SHA" -- README.md | grep -q .; then | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -18,8 +18,9 @@ | |||
| 18 | 18 | > [!NOTE] | |
| 19 | 19 | > **This README documents v1.x of the MCP Python SDK (the current stable release).** | |
| 20 | 20 | > | |
| 21 | - > For v1.x code and documentation, see the [`v1.x` branch](https://github.com/modelcontextprotocol/python-sdk/tree/v1.x). | ||
| 22 | - > For the upcoming v2 documentation (pre-alpha, in development on `main`), see [`README.v2.md`](README.v2.md). | ||
| 21 | + > **v2 is in alpha.** Pre-releases are published to PyPI as `2.0.0aN` and can be installed with an explicit pin, for example `pip install mcp==2.0.0a1`. See [`README.v2.md`](README.v2.md) for the v2 documentation and the [migration guide](docs/migration.md) for what's changed. We're targeting a beta on 2026-06-30 and a stable v2 on 2026-07-27. If your package depends on `mcp`, add a `<2` upper bound to your version constraint (for example `mcp>=1.27,<2`) before the stable release lands. | ||
| 22 | + > | ||
| 23 | + > For v1.x code and documentation, see the [`v1.x` branch](https://github.com/modelcontextprotocol/python-sdk/tree/v1.x). v1.x is in maintenance mode and continues to receive critical bug fixes and security patches. | ||
| 23 | 24 | ||
| 24 | 25 | <!-- omit in toc --> | |
| 25 | 26 | ## Table of Contents | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,5 +1,6 @@ | |||
| 1 | 1 | """Claude app integration utilities.""" | |
| 2 | 2 | ||
| 3 | + import importlib.metadata | ||
| 3 | 4 | import json | |
| 4 | 5 | import os | |
| 5 | 6 | import shutil | |
@@ -11,7 +12,24 @@ | |||
| 11 | 12 | ||
| 12 | 13 | logger = get_logger(__name__) | |
| 13 | 14 | ||
| 14 | - MCP_PACKAGE = "mcp[cli]" | ||
| 15 | + | ||
| 16 | + def mcp_requirement(package: str = "mcp") -> str: | ||
| 17 | + """Requirement string pinning spawned environments to the running SDK version. | ||
| 18 | + | ||
| 19 | + `uv run --with mcp` resolves the requirement in a fresh environment, where | ||
| 20 | + an unpinned `mcp` means the latest stable release — not necessarily the | ||
| 21 | + version the user installed (pre-releases in particular are never selected | ||
| 22 | + without an explicit pin). Source builds carry dev/local version segments | ||
| 23 | + that are not published to PyPI, so they fall back to the unpinned form, | ||
| 24 | + as does a missing distribution (no metadata to pin from). | ||
| 25 | + """ | ||
| 26 | + try: | ||
| 27 | + version = importlib.metadata.version("mcp") | ||
| 28 | + except importlib.metadata.PackageNotFoundError: | ||
| 29 | + return package | ||
| 30 | + if ".dev" in version or "+" in version: | ||
| 31 | + return package | ||
| 32 | + return f"{package}=={version}" | ||
| 15 | 33 | ||
| 16 | 34 | ||
| 17 | 35 | def get_claude_config_path() -> Path | None: # pragma: no cover | |
@@ -102,7 +120,7 @@ def update_claude_config( | |||
| 102 | 120 | args = ["run", "--frozen"] | |
| 103 | 121 | ||
| 104 | 122 | # Collect all packages in a set to deduplicate | |
| 105 | - packages = {MCP_PACKAGE} | ||
| 123 | + packages = {mcp_requirement("mcp[cli]")} | ||
| 106 | 124 | if with_packages: | |
| 107 | 125 | packages.update(pkg for pkg in with_packages if pkg) | |
| 108 | 126 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -70,7 +70,7 @@ def _build_uv_command( | |||
| 70 | 70 | """Build the uv run command that runs an MCP server through mcp run.""" | |
| 71 | 71 | cmd = ["uv"] | |
| 72 | 72 | ||
| 73 | - cmd.extend(["run", "--with", "mcp"]) | ||
| 73 | + cmd.extend(["run", "--with", claude.mcp_requirement()]) | ||
| 74 | 74 | ||
| 75 | 75 | if with_editable: | |
| 76 | 76 | cmd.extend(["--with-editable", str(with_editable)]) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,24 +1,68 @@ | |||
| 1 | 1 | """Tests for mcp.cli.claude — Claude Desktop config file generation.""" | |
| 2 | 2 | ||
| 3 | + import importlib.metadata | ||
| 3 | 4 | import json | |
| 4 | 5 | from pathlib import Path | |
| 5 | 6 | from typing import Any | |
| 6 | 7 | ||
| 7 | 8 | import pytest | |
| 8 | 9 | ||
| 9 | - from mcp.cli.claude import get_uv_path, update_claude_config | ||
| 10 | + from mcp.cli.claude import get_uv_path, mcp_requirement, update_claude_config | ||
| 11 | + | ||
| 12 | + | ||
| 13 | + def _set_mcp_version(monkeypatch: pytest.MonkeyPatch, version: str) -> None: | ||
| 14 | + real_version = importlib.metadata.version | ||
| 15 | + | ||
| 16 | + def fake_version(distribution_name: str) -> str: | ||
| 17 | + return version if distribution_name == "mcp" else real_version(distribution_name) | ||
| 18 | + | ||
| 19 | + monkeypatch.setattr(importlib.metadata, "version", fake_version) | ||
| 10 | 20 | ||
| 11 | 21 | ||
| 12 | 22 | @pytest.fixture | |
| 13 | 23 | def config_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: | |
| 14 | - """Temp Claude config dir with get_claude_config_path and get_uv_path mocked.""" | ||
| 24 | + """Temp Claude config dir with the config path, uv path, and SDK version mocked.""" | ||
| 15 | 25 | claude_dir = tmp_path / "Claude" | |
| 16 | 26 | claude_dir.mkdir() | |
| 17 | 27 | monkeypatch.setattr("mcp.cli.claude.get_claude_config_path", lambda: claude_dir) | |
| 18 | 28 | monkeypatch.setattr("mcp.cli.claude.get_uv_path", lambda: "/fake/bin/uv") | |
| 29 | + # The ambient version is a dev build in the repo venv but varies by | ||
| 30 | + # environment; pin it so the generated --with requirement is stable. | ||
| 31 | + _set_mcp_version(monkeypatch, "1.2.3") | ||
| 19 | 32 | return claude_dir | |
| 20 | 33 | ||
| 21 | 34 | ||
| 35 | + def test_mcp_requirement_pins_release_versions(monkeypatch: pytest.MonkeyPatch): | ||
| 36 | + """Release versions produce an exact pin so spawned environments run the installed SDK version.""" | ||
| 37 | + _set_mcp_version(monkeypatch, "2.0.0a1") | ||
| 38 | + assert mcp_requirement() == "mcp==2.0.0a1" | ||
| 39 | + assert mcp_requirement("mcp[cli]") == "mcp[cli]==2.0.0a1" | ||
| 40 | + | ||
| 41 | + | ||
| 42 | + def test_mcp_requirement_leaves_dev_versions_unpinned(monkeypatch: pytest.MonkeyPatch): | ||
| 43 | + """Dev versions are not published to PyPI, so the requirement falls back to the unpinned package.""" | ||
| 44 | + _set_mcp_version(monkeypatch, "2.0.0a2.dev3") | ||
| 45 | + assert mcp_requirement() == "mcp" | ||
| 46 | + assert mcp_requirement("mcp[cli]") == "mcp[cli]" | ||
| 47 | + | ||
| 48 | + | ||
| 49 | + def test_mcp_requirement_leaves_local_versions_unpinned(monkeypatch: pytest.MonkeyPatch): | ||
| 50 | + """Local version segments (source builds) are not published to PyPI, so no pin is emitted.""" | ||
| 51 | + _set_mcp_version(monkeypatch, "1.2.3+g0123abc") | ||
| 52 | + assert mcp_requirement() == "mcp" | ||
| 53 | + | ||
| 54 | + | ||
| 55 | + def test_mcp_requirement_falls_back_when_mcp_is_not_installed(monkeypatch: pytest.MonkeyPatch): | ||
| 56 | + """Without distribution metadata there is no version to pin, so the requirement stays unpinned.""" | ||
| 57 | + | ||
| 58 | + def raise_not_found(distribution_name: str) -> str: | ||
| 59 | + raise importlib.metadata.PackageNotFoundError(distribution_name) | ||
| 60 | + | ||
| 61 | + monkeypatch.setattr(importlib.metadata, "version", raise_not_found) | ||
| 62 | + assert mcp_requirement() == "mcp" | ||
| 63 | + assert mcp_requirement("mcp[cli]") == "mcp[cli]" | ||
| 64 | + | ||
| 65 | + | ||
| 22 | 66 | def _read_server(config_dir: Path, name: str) -> dict[str, Any]: | |
| 23 | 67 | config = json.loads((config_dir / "claude_desktop_config.json").read_text()) | |
| 24 | 68 | return config["mcpServers"][name] | |
@@ -31,7 +75,7 @@ def test_generates_uv_run_command(config_dir: Path): | |||
| 31 | 75 | resolved = Path("server.py").resolve() | |
| 32 | 76 | assert _read_server(config_dir, "my_server") == { | |
| 33 | 77 | "command": "/fake/bin/uv", | |
| 34 | - "args": ["run", "--frozen", "--with", "mcp[cli]", "mcp", "run", f"{resolved}:app"], | ||
| 78 | + "args": ["run", "--frozen", "--with", "mcp[cli]==1.2.3", "mcp", "run", f"{resolved}:app"], | ||
| 35 | 79 | } | |
| 36 | 80 | ||
| 37 | 81 | ||
@@ -43,11 +87,19 @@ def test_file_spec_without_object_suffix(config_dir: Path): | |||
| 43 | 87 | ||
| 44 | 88 | ||
| 45 | 89 | def test_with_packages_sorted_and_deduplicated(config_dir: Path): | |
| 46 | - """Extra packages should appear as --with flags, sorted and deduplicated with mcp[cli].""" | ||
| 90 | + """Extra packages should appear as sorted --with flags with duplicates removed.""" | ||
| 47 | 91 | assert update_claude_config(file_spec="s.py:app", server_name="s", with_packages=["zebra", "aardvark", "zebra"]) | |
| 48 | 92 | ||
| 49 | 93 | args = _read_server(config_dir, "s")["args"] | |
| 50 | - assert args[:8] == ["run", "--frozen", "--with", "aardvark", "--with", "mcp[cli]", "--with", "zebra"] | ||
| 94 | + assert args[:8] == ["run", "--frozen", "--with", "aardvark", "--with", "mcp[cli]==1.2.3", "--with", "zebra"] | ||
| 95 | + | ||
| 96 | + | ||
| 97 | + def test_explicit_mcp_cli_kept_alongside_pinned_requirement(config_dir: Path): | ||
| 98 | + """A user-supplied mcp[cli] no longer collapses into the pinned requirement; uv resolves both to the pin.""" | ||
| 99 | + assert update_claude_config(file_spec="s.py:app", server_name="s", with_packages=["mcp[cli]"]) | ||
| 100 | + | ||
| 101 | + args = _read_server(config_dir, "s")["args"] | ||
| 102 | + assert args[:6] == ["run", "--frozen", "--with", "mcp[cli]", "--with", "mcp[cli]==1.2.3"] | ||
| 51 | 103 | ||
| 52 | 104 | ||
| 53 | 105 | def test_with_editable_adds_flag(config_dir: Path, tmp_path: Path): | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,3 +1,4 @@ | |||
| 1 | + import importlib.metadata | ||
| 1 | 2 | import subprocess | |
| 2 | 3 | import sys | |
| 3 | 4 | from pathlib import Path | |
@@ -8,6 +9,15 @@ | |||
| 8 | 9 | from mcp.cli.cli import _build_uv_command, _get_npx_command, _parse_file_path # type: ignore[reportPrivateUsage] | |
| 9 | 10 | ||
| 10 | 11 | ||
| 12 | + def _set_mcp_version(monkeypatch: pytest.MonkeyPatch, version: str) -> None: | ||
| 13 | + real_version = importlib.metadata.version | ||
| 14 | + | ||
| 15 | + def fake_version(distribution_name: str) -> str: | ||
| 16 | + return version if distribution_name == "mcp" else real_version(distribution_name) | ||
| 17 | + | ||
| 18 | + monkeypatch.setattr(importlib.metadata, "version", fake_version) | ||
| 19 | + | ||
| 20 | + | ||
| 11 | 21 | @pytest.mark.parametrize( | |
| 12 | 22 | "spec, expected_obj", | |
| 13 | 23 | [ | |
@@ -38,14 +48,23 @@ def test_parse_file_exit_on_dir(tmp_path: Path): | |||
| 38 | 48 | _parse_file_path(str(dir_path)) | |
| 39 | 49 | ||
| 40 | 50 | ||
| 41 | - def test_build_uv_command_minimal(): | ||
| 42 | - """Should emit core command when no extras specified.""" | ||
| 51 | + def test_build_uv_command_pins_the_running_mcp_version(monkeypatch: pytest.MonkeyPatch): | ||
| 52 | + """The spawned environment installs the same SDK version that is running, not the latest stable.""" | ||
| 53 | + _set_mcp_version(monkeypatch, "1.2.3") | ||
| 54 | + cmd = _build_uv_command("foo.py") | ||
| 55 | + assert cmd == ["uv", "run", "--with", "mcp==1.2.3", "mcp", "run", "foo.py"] | ||
| 56 | + | ||
| 57 | + | ||
| 58 | + def test_build_uv_command_leaves_source_builds_unpinned(monkeypatch: pytest.MonkeyPatch): | ||
| 59 | + """Source-build versions are not on PyPI, so the requirement stays unpinned.""" | ||
| 60 | + _set_mcp_version(monkeypatch, "2.0.0a2.dev3+g0123abc") | ||
| 43 | 61 | cmd = _build_uv_command("foo.py") | |
| 44 | 62 | assert cmd == ["uv", "run", "--with", "mcp", "mcp", "run", "foo.py"] | |
| 45 | 63 | ||
| 46 | 64 | ||
| 47 | - def test_build_uv_command_adds_editable_and_packages(): | ||
| 65 | + def test_build_uv_command_adds_editable_and_packages(monkeypatch: pytest.MonkeyPatch): | ||
| 48 | 66 | """Should include --with-editable and every --with pkg in correct order.""" | |
| 67 | + _set_mcp_version(monkeypatch, "1.2.3") | ||
| 49 | 68 | test_path = Path("/pkg") | |
| 50 | 69 | cmd = _build_uv_command( | |
| 51 | 70 | "foo.py", | |
@@ -56,7 +75,7 @@ def test_build_uv_command_adds_editable_and_packages(): | |||
| 56 | 75 | "uv", | |
| 57 | 76 | "run", | |
| 58 | 77 | "--with", | |
| 59 | - "mcp", | ||
| 78 | + "mcp==1.2.3", | ||
| 60 | 79 | "--with-editable", | |
| 61 | 80 | str(test_path), # Use str() to match what the function does | |
| 62 | 81 | "--with", | |
| Back | FazBrowse Home | New Git URL |
0 commit comments