Skip to content

Commit ae217fd

Browse files
committed
Update the v2 status banner and pin spawned environments to the running SDK version
- README.md banner: v2 is in alpha with pre-releases published to PyPI (previously said "pre-alpha, in development"), plus the beta/stable target dates, the <2 upper-bound ask for dependents, and v1.x's maintenance-mode status. The freeze check in shared.yml now honors an override-readme-freeze label so deliberate updates like this one can land while accidental edits stay blocked. - mcp dev and mcp install spawn environments via `uv run --with mcp`, which resolves to the latest stable release rather than the version the user installed; pre-releases are never selected without an explicit pin, so with a v2 pre-release installed the spawned environment got v1 and the user's server failed to import. The requirement now pins the running version; source builds fall back to the unpinned form since dev/local versions are not published to PyPI.
1 parent e196857 commit ae217fd

6 files changed

Lines changed: 82 additions & 15 deletions

File tree

.github/workflows/shared.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@ jobs:
3030
env:
3131
SKIP: no-commit-to-branch,readme-v1-frozen
3232

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.
3435
- 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')
3637
run: |
3738
git fetch --no-tags --depth=1 origin "$BASE_SHA"
3839
if git diff --name-only "$BASE_SHA" -- README.md | grep -q .; then

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818
> [!NOTE]
1919
> **This README documents v1.x of the MCP Python SDK (the current stable release).**
2020
>
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.
2324
2425
<!-- omit in toc -->
2526
## Table of Contents

src/mcp/cli/claude.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Claude app integration utilities."""
22

3+
import importlib.metadata
34
import json
45
import os
56
import shutil
@@ -11,7 +12,20 @@
1112

1213
logger = get_logger(__name__)
1314

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+
"""
25+
version = importlib.metadata.version("mcp")
26+
if ".dev" in version or "+" in version:
27+
return package
28+
return f"{package}=={version}"
1529

1630

1731
def get_claude_config_path() -> Path | None: # pragma: no cover
@@ -102,7 +116,7 @@ def update_claude_config(
102116
args = ["run", "--frozen"]
103117

104118
# Collect all packages in a set to deduplicate
105-
packages = {MCP_PACKAGE}
119+
packages = {mcp_requirement("mcp[cli]")}
106120
if with_packages:
107121
packages.update(pkg for pkg in with_packages if pkg)
108122

src/mcp/cli/cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def _build_uv_command(
7070
"""Build the uv run command that runs an MCP server through mcp run."""
7171
cmd = ["uv"]
7272

73-
cmd.extend(["run", "--with", "mcp"])
73+
cmd.extend(["run", "--with", claude.mcp_requirement()])
7474

7575
if with_editable:
7676
cmd.extend(["--with-editable", str(with_editable)])

tests/cli/test_claude.py

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,56 @@
11
"""Tests for mcp.cli.claude — Claude Desktop config file generation."""
22

3+
import importlib.metadata
34
import json
45
from pathlib import Path
56
from typing import Any
67

78
import pytest
89

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)
1020

1121

1222
@pytest.fixture
1323
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."""
1525
claude_dir = tmp_path / "Claude"
1626
claude_dir.mkdir()
1727
monkeypatch.setattr("mcp.cli.claude.get_claude_config_path", lambda: claude_dir)
1828
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")
1932
return claude_dir
2033

2134

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+
47+
48+
def test_mcp_requirement_leaves_local_versions_unpinned(monkeypatch: pytest.MonkeyPatch):
49+
"""Local version segments (source builds) are not published to PyPI, so no pin is emitted."""
50+
_set_mcp_version(monkeypatch, "1.2.3+g0123abc")
51+
assert mcp_requirement() == "mcp"
52+
53+
2254
def _read_server(config_dir: Path, name: str) -> dict[str, Any]:
2355
config = json.loads((config_dir / "claude_desktop_config.json").read_text())
2456
return config["mcpServers"][name]
@@ -31,7 +63,7 @@ def test_generates_uv_run_command(config_dir: Path):
3163
resolved = Path("server.py").resolve()
3264
assert _read_server(config_dir, "my_server") == {
3365
"command": "/fake/bin/uv",
34-
"args": ["run", "--frozen", "--with", "mcp[cli]", "mcp", "run", f"{resolved}:app"],
66+
"args": ["run", "--frozen", "--with", "mcp[cli]==1.2.3", "mcp", "run", f"{resolved}:app"],
3567
}
3668

3769

@@ -47,7 +79,7 @@ def test_with_packages_sorted_and_deduplicated(config_dir: Path):
4779
assert update_claude_config(file_spec="s.py:app", server_name="s", with_packages=["zebra", "aardvark", "zebra"])
4880

4981
args = _read_server(config_dir, "s")["args"]
50-
assert args[:8] == ["run", "--frozen", "--with", "aardvark", "--with", "mcp[cli]", "--with", "zebra"]
82+
assert args[:8] == ["run", "--frozen", "--with", "aardvark", "--with", "mcp[cli]==1.2.3", "--with", "zebra"]
5183

5284

5385
def test_with_editable_adds_flag(config_dir: Path, tmp_path: Path):

tests/cli/test_utils.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import importlib.metadata
12
import subprocess
23
import sys
34
from pathlib import Path
@@ -8,6 +9,15 @@
89
from mcp.cli.cli import _build_uv_command, _get_npx_command, _parse_file_path # type: ignore[reportPrivateUsage]
910

1011

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+
1121
@pytest.mark.parametrize(
1222
"spec, expected_obj",
1323
[
@@ -38,14 +48,23 @@ def test_parse_file_exit_on_dir(tmp_path: Path):
3848
_parse_file_path(str(dir_path))
3949

4050

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")
4361
cmd = _build_uv_command("foo.py")
4462
assert cmd == ["uv", "run", "--with", "mcp", "mcp", "run", "foo.py"]
4563

4664

47-
def test_build_uv_command_adds_editable_and_packages():
65+
def test_build_uv_command_adds_editable_and_packages(monkeypatch: pytest.MonkeyPatch):
4866
"""Should include --with-editable and every --with pkg in correct order."""
67+
_set_mcp_version(monkeypatch, "1.2.3")
4968
test_path = Path("/pkg")
5069
cmd = _build_uv_command(
5170
"foo.py",
@@ -56,7 +75,7 @@ def test_build_uv_command_adds_editable_and_packages():
5675
"uv",
5776
"run",
5877
"--with",
59-
"mcp",
78+
"mcp==1.2.3",
6079
"--with-editable",
6180
str(test_path), # Use str() to match what the function does
6281
"--with",

0 commit comments

Comments
 (0)