-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Expand file tree
/
Copy pathtest_claude.py
More file actions
146 lines (98 loc) · 5.86 KB
/
test_claude.py
File metadata and controls
146 lines (98 loc) · 5.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
"""Tests for mcp.cli.claude — Claude Desktop config file generation."""
import json
from pathlib import Path
from typing import Any
import pytest
from mcp.cli.claude import get_uv_path, update_claude_config
@pytest.fixture
def config_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
"""Temp Claude config dir with get_claude_config_path and get_uv_path mocked."""
claude_dir = tmp_path / "Claude"
claude_dir.mkdir()
monkeypatch.setattr("mcp.cli.claude.get_claude_config_path", lambda: claude_dir)
monkeypatch.setattr("mcp.cli.claude.get_uv_path", lambda: "/fake/bin/uv")
return claude_dir
def _read_server(config_dir: Path, name: str) -> dict[str, Any]:
config = json.loads((config_dir / "claude_desktop_config.json").read_text())
return config["mcpServers"][name]
def test_generates_uv_run_command(config_dir: Path):
"""Should write a uv run command that invokes mcp run on the resolved file spec."""
assert update_claude_config(file_spec="server.py:app", server_name="my_server")
resolved = Path("server.py").resolve()
assert _read_server(config_dir, "my_server") == {
"command": "/fake/bin/uv",
"args": ["run", "--frozen", "--with", "mcp[cli]", "mcp", "run", f"{resolved}:app"],
}
def test_file_spec_without_object_suffix(config_dir: Path):
"""File specs without :object should still resolve to an absolute path."""
assert update_claude_config(file_spec="server.py", server_name="s")
assert _read_server(config_dir, "s")["args"][-1] == str(Path("server.py").resolve())
def test_with_packages_sorted_and_deduplicated(config_dir: Path):
"""Extra packages should appear as --with flags, sorted and deduplicated with mcp[cli]."""
assert update_claude_config(file_spec="s.py:app", server_name="s", with_packages=["zebra", "aardvark", "zebra"])
args = _read_server(config_dir, "s")["args"]
assert args[:8] == ["run", "--frozen", "--with", "aardvark", "--with", "mcp[cli]", "--with", "zebra"]
def test_with_editable_adds_flag(config_dir: Path, tmp_path: Path):
"""with_editable should add --with-editable after the --with flags."""
editable = tmp_path / "project"
assert update_claude_config(file_spec="s.py:app", server_name="s", with_editable=editable)
args = _read_server(config_dir, "s")["args"]
assert args[4:6] == ["--with-editable", str(editable)]
def test_env_vars_written(config_dir: Path):
"""env_vars should be written under the server's env key."""
assert update_claude_config(file_spec="s.py:app", server_name="s", env_vars={"KEY": "val"})
assert _read_server(config_dir, "s")["env"] == {"KEY": "val"}
def test_existing_env_vars_merged_new_wins(config_dir: Path):
"""Re-installing should merge env vars, with new values overriding existing ones."""
(config_dir / "claude_desktop_config.json").write_text(
json.dumps({"mcpServers": {"s": {"env": {"OLD": "keep", "KEY": "old"}}}})
)
assert update_claude_config(file_spec="s.py:app", server_name="s", env_vars={"KEY": "new"})
assert _read_server(config_dir, "s")["env"] == {"OLD": "keep", "KEY": "new"}
def test_existing_env_vars_preserved_without_new(config_dir: Path):
"""Re-installing without env_vars should keep the existing env block intact."""
(config_dir / "claude_desktop_config.json").write_text(json.dumps({"mcpServers": {"s": {"env": {"KEEP": "me"}}}}))
assert update_claude_config(file_spec="s.py:app", server_name="s")
assert _read_server(config_dir, "s")["env"] == {"KEEP": "me"}
def test_other_servers_preserved(config_dir: Path):
"""Installing a new server should not clobber existing mcpServers entries."""
(config_dir / "claude_desktop_config.json").write_text(json.dumps({"mcpServers": {"other": {"command": "x"}}}))
assert update_claude_config(file_spec="s.py:app", server_name="s")
config = json.loads((config_dir / "claude_desktop_config.json").read_text())
assert set(config["mcpServers"]) == {"other", "s"}
assert config["mcpServers"]["other"] == {"command": "x"}
def test_raises_when_config_dir_missing(monkeypatch: pytest.MonkeyPatch):
"""Should raise RuntimeError when Claude Desktop config dir can't be found."""
monkeypatch.setattr("mcp.cli.claude.get_claude_config_path", lambda: None)
monkeypatch.setattr("mcp.cli.claude.get_uv_path", lambda: "/fake/bin/uv")
with pytest.raises(RuntimeError, match="Claude Desktop config directory not found"):
update_claude_config(file_spec="s.py:app", server_name="s")
@pytest.mark.parametrize("which_result, expected", [("/usr/local/bin/uv", "/usr/local/bin/uv"), (None, "uv")])
def test_get_uv_path(monkeypatch: pytest.MonkeyPatch, which_result: str | None, expected: str):
"""Should return shutil.which's result, or fall back to bare 'uv' when not on PATH."""
def fake_which(cmd: str) -> str | None:
return which_result
monkeypatch.setattr("shutil.which", fake_which)
assert get_uv_path() == expected
@pytest.mark.parametrize(
"file_spec, expected_last_arg",
[
("C:\\Users\\server.py", "C:\\Users\\server.py"),
("C:\\Users\\server.py:app", "C:\\Users\\server.py:app"),
],
)
def test_windows_drive_letter_not_split(
config_dir: Path, monkeypatch: pytest.MonkeyPatch, file_spec: str, expected_last_arg: str
):
"""Drive-letter paths like 'C:\\server.py' must not be split on the drive colon.
Before the fix, a bare 'C:\\path\\server.py' would hit rsplit(":", 1) and yield
("C", "\\path\\server.py"), calling resolve() on Path("C") instead of the full path.
"""
seen: list[str] = []
def fake_resolve(self: Path) -> Path:
seen.append(str(self))
return self
monkeypatch.setattr(Path, "resolve", fake_resolve)
assert update_claude_config(file_spec=file_spec, server_name="s")
assert seen == ["C:\\Users\\server.py"]
assert _read_server(config_dir, "s")["args"][-1] == expected_last_arg