Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion QUICKSTART.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ Repo knowledge lives at the edge in `adapters/<repo>/` (human-gated
`manifest.yaml` + agent-established, evidence-gated `profile/`) — never in
`src/`. `PROFILE_BRIEFING_ENABLED=0` runs the {no-profile} eval ablation arm.

## 10. Advanced: MCP server (Claude Code / Codex / Cursor)
## 10. Advanced: MCP server (Claude Code / Codex / Cursor / ZCode)

```bash
pip install -e '.[mcp]' # optional extra, kept out of the base install
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@ install.cmd --repo-path D:\path\to\vllm-omni
./install-mcp.sh --repo-path /path/to/vllm-omni
```

安装器会识别本机的 Codex、Claude Code 和 Cursor,并安装 MCP、`imreview`、
安装器会识别本机的 Codex、Claude Code、CursorZCode,并安装 MCP、`imreview`、
`imdesign`、`imcifix` 和 `imupdate` Skill。未识别到已知 Agent 时,会生成标准
`infermatrix-copilot.mcp.json` 供其他 MCP 客户端导入。

Codex 重启后用 `$imreview` / `$imdesign` / `$imcifix` / `$imupdate`,也可以先运行 `/skills` 查找;
`/imreview` 不是 Codex 的 Slash Command。Claude Code 和 Cursor 仍使用
`/imreview` 不是 Codex 的 Slash Command。Claude Code、CursorZCode 仍使用
`/imreview` / `/imdesign` / `/imcifix` / `/imupdate`。

安装器还会创建 `~/.infermatrix-copilot/.env`。Direct 不需要模型密钥;使用
Expand Down Expand Up @@ -84,7 +84,7 @@ Use InferMatrixCopilot in Direct mode to review this PR.
$imreview https://github.com/vllm-project/vllm-omni/pull/5172
$imreview

# Claude Code / Cursor
# Claude Code / Cursor / ZCode
/imreview https://github.com/vllm-project/vllm-omni/pull/5172
或者说
“帮我审核一下这个pr xxxx,用知识库”
Expand Down
6 changes: 3 additions & 3 deletions doc/MCP.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ install.cmd --repo-path D:\path\to\vllm-omni
两个入口共用 `scripts/install_mcp.py`,但用户不需要安装或直接运行 Python:
入口会自动安装 `uv`,再由 `uv` 提供隔离运行环境。安装器会:

- 自动识别 Codex、Claude Code 和 Cursor,检测到几个就安装几个;
- 保留 Cursor 已有配置并先备份;
- 自动识别 Codex、Claude Code、CursorZCode,检测到几个就安装几个;
- 保留 Cursor / ZCode 已有配置并先备份;
- 没识别到已知 Agent 时生成 `infermatrix-copilot.mcp.json`,不会直接失败。
- 创建 `~/.infermatrix-copilot/.env` 作为 Strict 的稳定配置入口;安装时传入
`--repo-path` 会写入本地 vLLM-Omni checkout。模型密钥不会自动复制,使用
Expand All @@ -42,7 +42,7 @@ install.cmd --repo-path D:\path\to\vllm-omni
$imreview <PR URL>
$imupdate <local path, repository name, alias, or URL>

# Claude Code / Cursor
# Claude Code / Cursor / ZCode
/imreview <PR URL>
/imupdate <local path, repository name, alias, or URL>
```
Expand Down
61 changes: 59 additions & 2 deletions scripts/install_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,60 @@ def _install_cursor(config_root: Path) -> None:
_install_skills(cursor_root / "skills")


def _install_zcode(config_root: Path) -> None:
zcode_root = config_root / ".zcode"
config_path = zcode_root / "cli" / "config.json"
config_path.parent.mkdir(parents=True, exist_ok=True)

if config_path.exists():
try:
config = json.loads(config_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
raise InstallError(
f"ZCode config is invalid and was not changed: {config_path}"
) from exc
if not isinstance(config, dict):
raise InstallError(
f"ZCode config must be an object: {config_path}"
)
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
shutil.copy2(
config_path,
config_path.with_name(f"{config_path.name}.{timestamp}.bak"),
)
else:
config = {}

# ZCode nests servers under mcp.servers and also keeps hooks and plugin
# state in this file, so merge instead of rewriting the document.
mcp = config.setdefault("mcp", {})
if not isinstance(mcp, dict):
raise InstallError(
f"ZCode config mcp section must be an object: {config_path}"
)
servers = mcp.setdefault("servers", {})
if not isinstance(servers, dict):
raise InstallError(
f"ZCode config mcp.servers must be an object: {config_path}"
)
servers[SERVER_NAME] = {
"type": "stdio",
"command": SERVER_COMMAND[0],
"args": SERVER_COMMAND[1:],
}
config_path.write_text(
json.dumps(config, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
_install_skills(zcode_root / "skills")


def _zcode_installed(config_root: Path) -> bool:
if shutil.which("zcode"):
return True
return (config_root / ".zcode").exists()


def _cursor_installed(config_root: Path) -> bool:
if shutil.which("cursor") or shutil.which("cursor-agent"):
return True
Expand All @@ -311,6 +365,8 @@ def _detect_agents(config_root: Path) -> list[str]:
agents.append("claude")
if _cursor_installed(config_root):
agents.append("cursor")
if _zcode_installed(config_root):
agents.append("zcode")
return agents


Expand Down Expand Up @@ -339,7 +395,7 @@ def _parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace:
parser.add_argument(
"--agent",
action="append",
choices=("codex", "claude", "cursor"),
choices=("codex", "claude", "cursor", "zcode"),
help="Override automatic Agent detection. May be repeated.",
)
parser.add_argument(
Expand Down Expand Up @@ -392,6 +448,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
"codex": _install_codex,
"claude": _install_claude,
"cursor": _install_cursor,
"zcode": _install_zcode,
}
for agent in agents:
try:
Expand All @@ -415,7 +472,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
)
if any(agent != "codex" for agent in agents):
print(
"Claude/Cursor: restart, then run: /imreview <PR URL>, "
"Claude/Cursor/ZCode: restart, then run: /imreview <PR URL>, "
"/imcifix <issue URL>, or /imupdate <repository>"
)
return 0
Expand Down
66 changes: 66 additions & 0 deletions test/test_install_mcp_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import json
from pathlib import Path

import pytest


ROOT = Path(__file__).resolve().parents[1]
INSTALLER = ROOT / "scripts" / "install_mcp.py"
Expand Down Expand Up @@ -103,6 +105,70 @@ def test_cursor_install_preserves_existing_config(tmp_path):
assert str(ROOT) in text


def test_zcode_install_merges_nested_mcp_servers(tmp_path):
installer = _load_installer()
config_path = tmp_path / ".zcode" / "cli" / "config.json"
config_path.parent.mkdir(parents=True)
config_path.write_text(
json.dumps(
{
"plugins": {"keep": True},
"mcp": {"servers": {"existing": {"command": "existing"}}},
}
),
encoding="utf-8",
)

installer._install_zcode(tmp_path)

config = json.loads(config_path.read_text(encoding="utf-8"))
assert config["plugins"] == {"keep": True}
assert config["mcp"]["servers"]["existing"]["command"] == "existing"
server = config["mcp"]["servers"]["infermatrix-copilot"]
assert server["type"] == "stdio"
assert server["command"] == "uvx"
assert "infermatrix-copilot-mcp" in server["args"]
assert list(config_path.parent.glob("config.json.*.bak"))
assert (tmp_path / ".zcode" / "skills" / "imreview" / "SKILL.md").is_file()
update_skill = tmp_path / ".zcode" / "skills" / "imupdate" / "SKILL.md"
assert "{{INFERMATRIX_COPILOT_ROOT}}" not in update_skill.read_text(
encoding="utf-8"
)


def test_zcode_install_creates_config_when_missing(tmp_path):
installer = _load_installer()

installer._install_zcode(tmp_path)

config_path = tmp_path / ".zcode" / "cli" / "config.json"
config = json.loads(config_path.read_text(encoding="utf-8"))
assert config["mcp"]["servers"]["infermatrix-copilot"]["command"] == "uvx"
assert (tmp_path / ".zcode" / "skills" / "imdesign" / "SKILL.md").is_file()


def test_zcode_install_rejects_invalid_config(tmp_path):
installer = _load_installer()
config_path = tmp_path / ".zcode" / "cli" / "config.json"
config_path.parent.mkdir(parents=True)
config_path.write_text("{ not json", encoding="utf-8")

with pytest.raises(installer.InstallError):
installer._install_zcode(tmp_path)

assert config_path.read_text(encoding="utf-8") == "{ not json"


def test_zcode_detected_via_config_dir(monkeypatch, tmp_path):
installer = _load_installer()
monkeypatch.setattr(installer.shutil, "which", lambda command: None)

assert not installer._zcode_installed(tmp_path)
(tmp_path / ".zcode").mkdir()
assert installer._zcode_installed(tmp_path)
assert "zcode" in installer._detect_agents(tmp_path)


def test_codex_install_sets_timeout_and_installs_current_skill_location(
monkeypatch, tmp_path
):
Expand Down
Loading