Update the v2 status banner and pin spawned environments to the running SDK version (#2834)
This commit is contained in:
parent
e196857bc9
commit
1e21814ed5
6 changed files with 107 additions and 16 deletions
5
.github/workflows/shared.yml
vendored
5
.github/workflows/shared.yml
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)])
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue