Update the v2 status banner and pin spawned environments to the running SDK version (#2834)
Some checks failed
CI / checks (push) Has been cancelled
CI / all-green (push) Has been cancelled

This commit is contained in:
Max 2026-06-11 09:41:19 +01:00 • committed by GitHub
commit 1e21814ed5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 107 additions and 16 deletions

View file

@ -30,9 +30,10 @@ jobs:
env: env:
SKIP: no-commit-to-branch,readme-v1-frozen SKIP: no-commit-to-branch,readme-v1-frozen
# TODO(Max): Drop this in v2. # TODO(Max): Drop this in v2. Deliberate updates (e.g. the v2 status
# banner) go through the 'override-readme-freeze' label.
- name: Check README.md is not modified - name: Check README.md is not modified
if: github.event_name == 'pull_request' if: github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'override-readme-freeze')
run: | run: |
git fetch --no-tags --depth=1 origin "$BASE_SHA" git fetch --no-tags --depth=1 origin "$BASE_SHA"
if git diff --name-only "$BASE_SHA" -- README.md | grep -q .; then if git diff --name-only "$BASE_SHA" -- README.md | grep -q .; then

View file

@ -18,8 +18,9 @@
> [!NOTE] > [!NOTE]
> **This README documents v1.x of the MCP Python SDK (the current stable release).** > **This README documents v1.x of the MCP Python SDK (the current stable release).**
> >
> For v1.x code and documentation, see the [`v1.x` branch](https://github.com/modelcontextprotocol/python-sdk/tree/v1.x). > **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.
> For the upcoming v2 documentation (pre-alpha, in development on `main`), see [`README.v2.md`](README.v2.md). >
> 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.
<!-- omit in toc --> <!-- omit in toc -->
## Table of Contents ## Table of Contents

View file

@ -1,5 +1,6 @@
"""Claude app integration utilities.""" """Claude app integration utilities."""
import importlib.metadata
import json import json
import os import os
import shutil import shutil
@ -11,7 +12,24 @@ from mcp.server.mcpserver.utilities.logging import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
MCP_PACKAGE = "mcp[cli]"
def mcp_requirement(package: str = "mcp") -> str:
"""Requirement string pinning spawned environments to the running SDK version.
`uv run --with mcp` resolves the requirement in a fresh environment, where
an unpinned `mcp` means the latest stable release — not necessarily the
version the user installed (pre-releases in particular are never selected
without an explicit pin). Source builds carry dev/local version segments
that are not published to PyPI, so they fall back to the unpinned form,
as does a missing distribution (no metadata to pin from).
"""
try:
version = importlib.metadata.version("mcp")
except importlib.metadata.PackageNotFoundError:
return package
if ".dev" in version or "+" in version:
return package
return f"{package}=={version}"
def get_claude_config_path() -> Path | None: # pragma: no cover def get_claude_config_path() -> Path | None: # pragma: no cover
@ -102,7 +120,7 @@ def update_claude_config(
args = ["run", "--frozen"] args = ["run", "--frozen"]
# Collect all packages in a set to deduplicate # Collect all packages in a set to deduplicate
packages = {MCP_PACKAGE} packages = {mcp_requirement("mcp[cli]")}
if with_packages: if with_packages:
packages.update(pkg for pkg in with_packages if pkg) packages.update(pkg for pkg in with_packages if pkg)

View file

@ -70,7 +70,7 @@ def _build_uv_command(
"""Build the uv run command that runs an MCP server through mcp run.""" """Build the uv run command that runs an MCP server through mcp run."""
cmd = ["uv"] cmd = ["uv"]
cmd.extend(["run", "--with", "mcp"]) cmd.extend(["run", "--with", claude.mcp_requirement()])
if with_editable: if with_editable:
cmd.extend(["--with-editable", str(with_editable)]) cmd.extend(["--with-editable", str(with_editable)])

View file

@ -1,24 +1,68 @@
"""Tests for mcp.cli.claude — Claude Desktop config file generation.""" """Tests for mcp.cli.claude — Claude Desktop config file generation."""
import importlib.metadata
import json import json
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
import pytest import pytest
from mcp.cli.claude import get_uv_path, update_claude_config from mcp.cli.claude import get_uv_path, mcp_requirement, update_claude_config
def _set_mcp_version(monkeypatch: pytest.MonkeyPatch, version: str) -> None:
real_version = importlib.metadata.version
def fake_version(distribution_name: str) -> str:
return version if distribution_name == "mcp" else real_version(distribution_name)
monkeypatch.setattr(importlib.metadata, "version", fake_version)
@pytest.fixture @pytest.fixture
def config_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: def config_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
"""Temp Claude config dir with get_claude_config_path and get_uv_path mocked.""" """Temp Claude config dir with the config path, uv path, and SDK version mocked."""
claude_dir = tmp_path / "Claude" claude_dir = tmp_path / "Claude"
claude_dir.mkdir() claude_dir.mkdir()
monkeypatch.setattr("mcp.cli.claude.get_claude_config_path", lambda: claude_dir) monkeypatch.setattr("mcp.cli.claude.get_claude_config_path", lambda: claude_dir)
monkeypatch.setattr("mcp.cli.claude.get_uv_path", lambda: "/fake/bin/uv") monkeypatch.setattr("mcp.cli.claude.get_uv_path", lambda: "/fake/bin/uv")
# The ambient version is a dev build in the repo venv but varies by
# environment; pin it so the generated --with requirement is stable.
_set_mcp_version(monkeypatch, "1.2.3")
return claude_dir return claude_dir
def test_mcp_requirement_pins_release_versions(monkeypatch: pytest.MonkeyPatch):
"""Release versions produce an exact pin so spawned environments run the installed SDK version."""
_set_mcp_version(monkeypatch, "2.0.0a1")
assert mcp_requirement() == "mcp==2.0.0a1"
assert mcp_requirement("mcp[cli]") == "mcp[cli]==2.0.0a1"
def test_mcp_requirement_leaves_dev_versions_unpinned(monkeypatch: pytest.MonkeyPatch):
"""Dev versions are not published to PyPI, so the requirement falls back to the unpinned package."""
_set_mcp_version(monkeypatch, "2.0.0a2.dev3")
assert mcp_requirement() == "mcp"
assert mcp_requirement("mcp[cli]") == "mcp[cli]"
def test_mcp_requirement_leaves_local_versions_unpinned(monkeypatch: pytest.MonkeyPatch):
"""Local version segments (source builds) are not published to PyPI, so no pin is emitted."""
_set_mcp_version(monkeypatch, "1.2.3+g0123abc")
assert mcp_requirement() == "mcp"
def test_mcp_requirement_falls_back_when_mcp_is_not_installed(monkeypatch: pytest.MonkeyPatch):
"""Without distribution metadata there is no version to pin, so the requirement stays unpinned."""
def raise_not_found(distribution_name: str) -> str:
raise importlib.metadata.PackageNotFoundError(distribution_name)
monkeypatch.setattr(importlib.metadata, "version", raise_not_found)
assert mcp_requirement() == "mcp"
assert mcp_requirement("mcp[cli]") == "mcp[cli]"
def _read_server(config_dir: Path, name: str) -> dict[str, Any]: def _read_server(config_dir: Path, name: str) -> dict[str, Any]:
config = json.loads((config_dir / "claude_desktop_config.json").read_text()) config = json.loads((config_dir / "claude_desktop_config.json").read_text())
return config["mcpServers"][name] return config["mcpServers"][name]
@ -31,7 +75,7 @@ def test_generates_uv_run_command(config_dir: Path):
resolved = Path("server.py").resolve() resolved = Path("server.py").resolve()
assert _read_server(config_dir, "my_server") == { assert _read_server(config_dir, "my_server") == {
"command": "/fake/bin/uv", "command": "/fake/bin/uv",
"args": ["run", "--frozen", "--with", "mcp[cli]", "mcp", "run", f"{resolved}:app"], "args": ["run", "--frozen", "--with", "mcp[cli]==1.2.3", "mcp", "run", f"{resolved}:app"],
} }
@ -43,11 +87,19 @@ def test_file_spec_without_object_suffix(config_dir: Path):
def test_with_packages_sorted_and_deduplicated(config_dir: Path): def test_with_packages_sorted_and_deduplicated(config_dir: Path):
"""Extra packages should appear as --with flags, sorted and deduplicated with mcp[cli].""" """Extra packages should appear as sorted --with flags with duplicates removed."""
assert update_claude_config(file_spec="s.py:app", server_name="s", with_packages=["zebra", "aardvark", "zebra"]) assert update_claude_config(file_spec="s.py:app", server_name="s", with_packages=["zebra", "aardvark", "zebra"])
args = _read_server(config_dir, "s")["args"] args = _read_server(config_dir, "s")["args"]
assert args[:8] == ["run", "--frozen", "--with", "aardvark", "--with", "mcp[cli]", "--with", "zebra"] assert args[:8] == ["run", "--frozen", "--with", "aardvark", "--with", "mcp[cli]==1.2.3", "--with", "zebra"]
def test_explicit_mcp_cli_kept_alongside_pinned_requirement(config_dir: Path):
"""A user-supplied mcp[cli] no longer collapses into the pinned requirement; uv resolves both to the pin."""
assert update_claude_config(file_spec="s.py:app", server_name="s", with_packages=["mcp[cli]"])
args = _read_server(config_dir, "s")["args"]
assert args[:6] == ["run", "--frozen", "--with", "mcp[cli]", "--with", "mcp[cli]==1.2.3"]
def test_with_editable_adds_flag(config_dir: Path, tmp_path: Path): def test_with_editable_adds_flag(config_dir: Path, tmp_path: Path):

View file

@ -1,3 +1,4 @@
import importlib.metadata
import subprocess import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
@ -8,6 +9,15 @@ import pytest
from mcp.cli.cli import _build_uv_command, _get_npx_command, _parse_file_path # type: ignore[reportPrivateUsage] from mcp.cli.cli import _build_uv_command, _get_npx_command, _parse_file_path # type: ignore[reportPrivateUsage]
def _set_mcp_version(monkeypatch: pytest.MonkeyPatch, version: str) -> None:
real_version = importlib.metadata.version
def fake_version(distribution_name: str) -> str:
return version if distribution_name == "mcp" else real_version(distribution_name)
monkeypatch.setattr(importlib.metadata, "version", fake_version)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"spec, expected_obj", "spec, expected_obj",
[ [
@ -38,14 +48,23 @@ def test_parse_file_exit_on_dir(tmp_path: Path):
_parse_file_path(str(dir_path)) _parse_file_path(str(dir_path))
def test_build_uv_command_minimal(): def test_build_uv_command_pins_the_running_mcp_version(monkeypatch: pytest.MonkeyPatch):
"""Should emit core command when no extras specified.""" """The spawned environment installs the same SDK version that is running, not the latest stable."""
_set_mcp_version(monkeypatch, "1.2.3")
cmd = _build_uv_command("foo.py")
assert cmd == ["uv", "run", "--with", "mcp==1.2.3", "mcp", "run", "foo.py"]
def test_build_uv_command_leaves_source_builds_unpinned(monkeypatch: pytest.MonkeyPatch):
"""Source-build versions are not on PyPI, so the requirement stays unpinned."""
_set_mcp_version(monkeypatch, "2.0.0a2.dev3+g0123abc")
cmd = _build_uv_command("foo.py") cmd = _build_uv_command("foo.py")
assert cmd == ["uv", "run", "--with", "mcp", "mcp", "run", "foo.py"] assert cmd == ["uv", "run", "--with", "mcp", "mcp", "run", "foo.py"]
def test_build_uv_command_adds_editable_and_packages(): def test_build_uv_command_adds_editable_and_packages(monkeypatch: pytest.MonkeyPatch):
"""Should include --with-editable and every --with pkg in correct order.""" """Should include --with-editable and every --with pkg in correct order."""
_set_mcp_version(monkeypatch, "1.2.3")
test_path = Path("/pkg") test_path = Path("/pkg")
cmd = _build_uv_command( cmd = _build_uv_command(
"foo.py", "foo.py",
@ -56,7 +75,7 @@ def test_build_uv_command_adds_editable_and_packages():
"uv", "uv",
"run", "run",
"--with", "--with",
"mcp", "mcp==1.2.3",
"--with-editable", "--with-editable",
str(test_path), # Use str() to match what the function does str(test_path), # Use str() to match what the function does
"--with", "--with",