evo-cli 0.8.1__tar.gz → 0.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. {evo_cli-0.8.1 → evo_cli-0.9.0}/PKG-INFO +1 -1
  2. evo_cli-0.9.0/evo_cli/VERSION +1 -0
  3. {evo_cli-0.8.1 → evo_cli-0.9.0}/evo_cli/cli.py +2 -0
  4. evo_cli-0.9.0/evo_cli/commands/mcp.py +234 -0
  5. {evo_cli-0.8.1 → evo_cli-0.9.0}/evo_cli/commands/opencode.py +2 -2
  6. {evo_cli-0.8.1 → evo_cli-0.9.0}/evo_cli.egg-info/PKG-INFO +1 -1
  7. {evo_cli-0.8.1 → evo_cli-0.9.0}/evo_cli.egg-info/SOURCES.txt +2 -0
  8. evo_cli-0.9.0/tests/test_mcp.py +88 -0
  9. evo_cli-0.8.1/evo_cli/VERSION +0 -1
  10. {evo_cli-0.8.1 → evo_cli-0.9.0}/Containerfile +0 -0
  11. {evo_cli-0.8.1 → evo_cli-0.9.0}/HISTORY.md +0 -0
  12. {evo_cli-0.8.1 → evo_cli-0.9.0}/LICENSE +0 -0
  13. {evo_cli-0.8.1 → evo_cli-0.9.0}/MANIFEST.in +0 -0
  14. {evo_cli-0.8.1 → evo_cli-0.9.0}/README.md +0 -0
  15. {evo_cli-0.8.1 → evo_cli-0.9.0}/evo_cli/__init__.py +0 -0
  16. {evo_cli-0.8.1 → evo_cli-0.9.0}/evo_cli/__main__.py +0 -0
  17. {evo_cli-0.8.1 → evo_cli-0.9.0}/evo_cli/base.py +0 -0
  18. {evo_cli-0.8.1 → evo_cli-0.9.0}/evo_cli/commands/__init__.py +0 -0
  19. {evo_cli-0.8.1 → evo_cli-0.9.0}/evo_cli/commands/cloudflare.py +0 -0
  20. {evo_cli-0.8.1 → evo_cli-0.9.0}/evo_cli/commands/fix_claude.py +0 -0
  21. {evo_cli-0.8.1 → evo_cli-0.9.0}/evo_cli/commands/gdrive.py +0 -0
  22. {evo_cli-0.8.1 → evo_cli-0.9.0}/evo_cli/commands/localproxy.py +0 -0
  23. {evo_cli-0.8.1 → evo_cli-0.9.0}/evo_cli/commands/miniconda.py +0 -0
  24. {evo_cli-0.8.1 → evo_cli-0.9.0}/evo_cli/commands/netcheck.py +0 -0
  25. {evo_cli-0.8.1 → evo_cli-0.9.0}/evo_cli/commands/plantuml.py +0 -0
  26. {evo_cli-0.8.1 → evo_cli-0.9.0}/evo_cli/commands/site2s.py +0 -0
  27. {evo_cli-0.8.1 → evo_cli-0.9.0}/evo_cli/commands/ssh.py +0 -0
  28. {evo_cli-0.8.1 → evo_cli-0.9.0}/evo_cli/commands/sysmon.py +0 -0
  29. {evo_cli-0.8.1 → evo_cli-0.9.0}/evo_cli/commands/wifi.py +0 -0
  30. {evo_cli-0.8.1 → evo_cli-0.9.0}/evo_cli/console.py +0 -0
  31. {evo_cli-0.8.1 → evo_cli-0.9.0}/evo_cli.egg-info/dependency_links.txt +0 -0
  32. {evo_cli-0.8.1 → evo_cli-0.9.0}/evo_cli.egg-info/entry_points.txt +0 -0
  33. {evo_cli-0.8.1 → evo_cli-0.9.0}/evo_cli.egg-info/requires.txt +0 -0
  34. {evo_cli-0.8.1 → evo_cli-0.9.0}/evo_cli.egg-info/top_level.txt +0 -0
  35. {evo_cli-0.8.1 → evo_cli-0.9.0}/pyproject.toml +0 -0
  36. {evo_cli-0.8.1 → evo_cli-0.9.0}/setup.cfg +0 -0
  37. {evo_cli-0.8.1 → evo_cli-0.9.0}/tests/__init__.py +0 -0
  38. {evo_cli-0.8.1 → evo_cli-0.9.0}/tests/test_cli.py +0 -0
  39. {evo_cli-0.8.1 → evo_cli-0.9.0}/tests/test_console.py +0 -0
  40. {evo_cli-0.8.1 → evo_cli-0.9.0}/tests/test_fix_claude.py +0 -0
  41. {evo_cli-0.8.1 → evo_cli-0.9.0}/tests/test_opencode.py +0 -0
  42. {evo_cli-0.8.1 → evo_cli-0.9.0}/tests/test_plantuml.py +0 -0
  43. {evo_cli-0.8.1 → evo_cli-0.9.0}/tests/test_sysmon.py +0 -0
  44. {evo_cli-0.8.1 → evo_cli-0.9.0}/tests/test_wifi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evo_cli
3
- Version: 0.8.1
3
+ Version: 0.9.0
4
4
  Summary: Evolution CLI - a developer toolbox for setting up dev machines
5
5
  Author: maycuatroi
6
6
  Project-URL: Homepage, https://github.com/maycuatroi/evo-cli
@@ -0,0 +1 @@
1
+ 0.9.0
@@ -5,6 +5,7 @@ from evo_cli.commands.cloudflare import cfssh
5
5
  from evo_cli.commands.fix_claude import f_claude
6
6
  from evo_cli.commands.gdrive import gdrive
7
7
  from evo_cli.commands.localproxy import localproxy
8
+ from evo_cli.commands.mcp import mcp_group
8
9
  from evo_cli.commands.miniconda import miniconda
9
10
  from evo_cli.commands.netcheck import netcheck
10
11
  from evo_cli.commands.opencode import setup_opencode
@@ -50,6 +51,7 @@ cli.add_command(f_claude)
50
51
  cli.add_command(gdrive)
51
52
  cli.add_command(site2s)
52
53
  cli.add_command(localproxy)
54
+ cli.add_command(mcp_group)
53
55
  cli.add_command(netcheck)
54
56
  cli.add_command(plantuml)
55
57
  cli.add_command(sysmon)
@@ -0,0 +1,234 @@
1
+ """Set up MCP servers for Claude Code and OpenCode in one shot."""
2
+
3
+ import shutil
4
+ import subprocess
5
+ from pathlib import Path
6
+
7
+ import rich_click as click
8
+ from rich.panel import Panel
9
+ from rich.table import Table
10
+ from rich.text import Text
11
+
12
+ from evo_cli.commands.opencode import (
13
+ get_global_config_path,
14
+ load_jsonc,
15
+ save_jsonc,
16
+ )
17
+ from evo_cli.console import console, error, info, run_command, step, success, warning
18
+
19
+ EPILOG = Text.from_markup(
20
+ "[bold]Examples[/bold]\n\n"
21
+ " [cyan]evo mcp list[/cyan]\n"
22
+ " [cyan]evo mcp add notion[/cyan]\n"
23
+ " [cyan]evo mcp add notion --claude-only[/cyan]\n"
24
+ " [cyan]evo mcp add notion --opencode-only --project .[/cyan]\n"
25
+ " [cyan]evo mcp add my-server --url https://example.com/mcp --transport http[/cyan]"
26
+ )
27
+
28
+ MCP_REGISTRY = {
29
+ "notion": {
30
+ "transport": "http",
31
+ "url": "https://mcp.notion.com/mcp",
32
+ "description": "Notion workspace - pages, databases, search. OAuth on first use.",
33
+ },
34
+ "context7": {
35
+ "transport": "http",
36
+ "url": "https://mcp.context7.com/mcp",
37
+ "description": "Context7 - up-to-date library docs and code examples.",
38
+ },
39
+ "deepwiki": {
40
+ "transport": "sse",
41
+ "url": "https://mcp.deepwiki.com/sse",
42
+ "description": "DeepWiki - ask questions about any public GitHub repo.",
43
+ },
44
+ }
45
+
46
+
47
+ def to_opencode_config(spec):
48
+ if spec["transport"] in ("http", "sse"):
49
+ config = {"type": "remote", "url": spec["url"], "enabled": True}
50
+ if spec.get("headers"):
51
+ config["headers"] = dict(spec["headers"])
52
+ return config
53
+ config = {"type": "local", "command": list(spec["command"]), "enabled": True}
54
+ if spec.get("env"):
55
+ config["environment"] = dict(spec["env"])
56
+ return config
57
+
58
+
59
+ def claude_has_server(name):
60
+ result = subprocess.run(
61
+ ["claude", "mcp", "get", name],
62
+ capture_output=True,
63
+ text=True,
64
+ )
65
+ return result.returncode == 0
66
+
67
+
68
+ def add_to_claude(name, spec, scope):
69
+ step("Adding to Claude Code")
70
+ if not shutil.which("claude"):
71
+ warning("`claude` CLI not found on PATH; skipping Claude Code")
72
+ info("Install Claude Code, then re-run to register this MCP server")
73
+ return False
74
+
75
+ if claude_has_server(name):
76
+ info(f"MCP server [accent]{name}[/accent] already in Claude Code; skipping")
77
+ return True
78
+
79
+ cmd = ["claude", "mcp", "add", "--scope", scope]
80
+ if spec["transport"] in ("http", "sse"):
81
+ cmd += ["--transport", spec["transport"], name, spec["url"]]
82
+ else:
83
+ for key, value in (spec.get("env") or {}).items():
84
+ cmd += ["-e", f"{key}={value}"]
85
+ cmd += [name, "--", *spec["command"]]
86
+ if spec.get("headers"):
87
+ for key, value in spec["headers"].items():
88
+ cmd += ["--header", f"{key}: {value}"]
89
+
90
+ try:
91
+ run_command(cmd)
92
+ except Exception as exc:
93
+ error(f"Could not add to Claude Code: {exc}")
94
+ return False
95
+ success(f"Added [accent]{name}[/accent] to Claude Code ({scope} scope)")
96
+ info("Run /mcp inside Claude Code to authenticate if the server needs OAuth")
97
+ return True
98
+
99
+
100
+ def add_to_opencode(name, spec, project):
101
+ step("Adding to OpenCode")
102
+ server_config = to_opencode_config(spec)
103
+
104
+ config_path = get_global_config_path()
105
+ existing = load_jsonc(config_path) if config_path.exists() else {}
106
+ mcp = existing.setdefault("mcp", {})
107
+ if name in mcp:
108
+ info(f"MCP server [accent]{name}[/accent] already in OpenCode global; skipping")
109
+ else:
110
+ mcp[name] = server_config
111
+ header = "// OpenCode global configuration generated by evo"
112
+ save_jsonc(config_path, existing, header=header)
113
+ success(f"Added [accent]{name}[/accent] to OpenCode global config")
114
+ info(f"Config: [accent]{config_path}[/accent]")
115
+
116
+ if project:
117
+ target = Path(project).resolve()
118
+ proj_path = target / "opencode.json"
119
+ proj_existing = load_jsonc(proj_path) if proj_path.exists() else {}
120
+ proj_mcp = proj_existing.setdefault("mcp", {})
121
+ if name in proj_mcp:
122
+ info(f"MCP server [accent]{name}[/accent] already in project config; skipping")
123
+ else:
124
+ proj_mcp[name] = server_config
125
+ header = "// OpenCode project configuration generated by evo"
126
+ save_jsonc(proj_path, proj_existing, header=header)
127
+ success(f"Added [accent]{name}[/accent] to project config [accent]{proj_path}[/accent]")
128
+ return True
129
+
130
+
131
+ def resolve_spec(name, url, transport):
132
+ if url:
133
+ if transport not in ("http", "sse"):
134
+ raise click.BadParameter("--transport must be http or sse when using --url")
135
+ return {"transport": transport, "url": url, "description": "custom"}
136
+ if name in MCP_REGISTRY:
137
+ return MCP_REGISTRY[name]
138
+ raise click.BadParameter(
139
+ f"Unknown MCP server '{name}'. Run `evo mcp list` to see known servers, or pass --url to add a custom one."
140
+ )
141
+
142
+
143
+ @click.group("mcp", help="Set up MCP servers for Claude Code and OpenCode.")
144
+ def mcp_group():
145
+ """Register MCP servers across your AI coding tools at once."""
146
+
147
+
148
+ @mcp_group.command("list")
149
+ def list_servers():
150
+ """List the MCP servers bundled in the registry."""
151
+ table = Table(title="Known MCP servers", title_style="step", expand=False)
152
+ table.add_column("Name", style="accent")
153
+ table.add_column("Transport", style="info")
154
+ table.add_column("Endpoint", style="dim")
155
+ table.add_column("Description")
156
+ for name, spec in MCP_REGISTRY.items():
157
+ endpoint = spec.get("url") or " ".join(spec.get("command", []))
158
+ table.add_row(name, spec["transport"], endpoint, spec["description"])
159
+ console.print(table)
160
+ console.print(
161
+ "\nAdd one with [accent]evo mcp add <name>[/accent], "
162
+ "or a custom server with [accent]evo mcp add <name> --url <url>[/accent]."
163
+ )
164
+
165
+
166
+ @mcp_group.command("add", epilog=EPILOG)
167
+ @click.argument("name")
168
+ @click.option("--url", help="Custom remote MCP endpoint (for servers not in the registry).")
169
+ @click.option(
170
+ "--transport",
171
+ type=click.Choice(["http", "sse"]),
172
+ default="http",
173
+ show_default=True,
174
+ help="Transport for a custom --url server.",
175
+ )
176
+ @click.option(
177
+ "--scope",
178
+ type=click.Choice(["local", "user", "project"]),
179
+ default="user",
180
+ show_default=True,
181
+ help="Claude Code config scope.",
182
+ )
183
+ @click.option("--claude-only", is_flag=True, help="Only configure Claude Code.")
184
+ @click.option("--opencode-only", is_flag=True, help="Only configure OpenCode.")
185
+ @click.option(
186
+ "--project",
187
+ type=click.Path(file_okay=False, dir_okay=True),
188
+ help="Also write into a project-level opencode.json at this directory.",
189
+ )
190
+ def add_server(name, url, transport, scope, claude_only, opencode_only, project):
191
+ """Add an MCP server to Claude Code and OpenCode.
192
+
193
+ NAME is a key from the registry (`evo mcp list`) or any label when paired
194
+ with --url for a custom remote server. By default the server is registered
195
+ in both tools; use --claude-only or --opencode-only to limit the scope.
196
+
197
+ Remote servers that use OAuth (like notion) prompt for sign-in on first use:
198
+ run /mcp inside Claude Code, and OpenCode triggers the flow on next launch.
199
+ """
200
+ if claude_only and opencode_only:
201
+ error("--claude-only and --opencode-only are mutually exclusive")
202
+ return
203
+
204
+ try:
205
+ spec = resolve_spec(name, url, transport)
206
+ except click.BadParameter as exc:
207
+ error(str(exc))
208
+ return
209
+
210
+ step(f"evo mcp add {name}")
211
+ info(spec.get("description", "custom"))
212
+
213
+ did_claude = did_opencode = False
214
+ if not opencode_only:
215
+ did_claude = add_to_claude(name, spec, scope)
216
+ if not claude_only:
217
+ did_opencode = add_to_opencode(name, spec, project)
218
+
219
+ console.print()
220
+ targets = []
221
+ if did_claude:
222
+ targets.append("Claude Code")
223
+ if did_opencode:
224
+ targets.append("OpenCode")
225
+ summary = ", ".join(targets) if targets else "no tools (check warnings above)"
226
+ console.print(
227
+ Panel(
228
+ f"MCP server [accent]{name}[/accent] configured for: {summary}.\n\n"
229
+ "Restart OpenCode and authenticate in Claude Code (/mcp) if prompted.",
230
+ title="mcp add complete",
231
+ border_style="success",
232
+ expand=False,
233
+ )
234
+ )
@@ -229,7 +229,7 @@ def configure_opencode_global():
229
229
  existing = load_jsonc(config_path) if config_path.exists() else {}
230
230
  merged = merge_mcp_config(existing, DEFAULT_MCP_SERVERS)
231
231
 
232
- header = "// OpenCode global configuration generated by evo setup opencode"
232
+ header = "// OpenCode global configuration generated by evo"
233
233
  save_jsonc(config_path, merged, header=header)
234
234
  success(f"Global config written to [accent]{config_path}[/accent]")
235
235
  return config_path
@@ -244,7 +244,7 @@ def configure_opencode_project(project_path):
244
244
  existing = load_jsonc(config_path) if config_path.exists() else {}
245
245
  merged = merge_mcp_config(existing, DEFAULT_MCP_SERVERS)
246
246
 
247
- header = "// OpenCode project configuration generated by evo setup opencode"
247
+ header = "// OpenCode project configuration generated by evo"
248
248
  save_jsonc(config_path, merged, header=header)
249
249
  success(f"Project config written to [accent]{config_path}[/accent]")
250
250
  return config_path
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evo_cli
3
- Version: 0.8.1
3
+ Version: 0.9.0
4
4
  Summary: Evolution CLI - a developer toolbox for setting up dev machines
5
5
  Author: maycuatroi
6
6
  Project-URL: Homepage, https://github.com/maycuatroi/evo-cli
@@ -21,6 +21,7 @@ evo_cli/commands/cloudflare.py
21
21
  evo_cli/commands/fix_claude.py
22
22
  evo_cli/commands/gdrive.py
23
23
  evo_cli/commands/localproxy.py
24
+ evo_cli/commands/mcp.py
24
25
  evo_cli/commands/miniconda.py
25
26
  evo_cli/commands/netcheck.py
26
27
  evo_cli/commands/opencode.py
@@ -33,6 +34,7 @@ tests/__init__.py
33
34
  tests/test_cli.py
34
35
  tests/test_console.py
35
36
  tests/test_fix_claude.py
37
+ tests/test_mcp.py
36
38
  tests/test_opencode.py
37
39
  tests/test_plantuml.py
38
40
  tests/test_sysmon.py
@@ -0,0 +1,88 @@
1
+ import pytest
2
+ from click.testing import CliRunner
3
+
4
+ from evo_cli.cli import cli
5
+ from evo_cli.commands.mcp import resolve_spec, to_opencode_config
6
+ from evo_cli.commands.opencode import load_jsonc
7
+
8
+
9
+ @pytest.fixture
10
+ def runner():
11
+ return CliRunner()
12
+
13
+
14
+ def test_mcp_command_is_registered():
15
+ assert "mcp" in cli.commands
16
+ assert "add" in cli.commands["mcp"].commands
17
+ assert "list" in cli.commands["mcp"].commands
18
+
19
+
20
+ def test_mcp_list_runs(runner):
21
+ result = runner.invoke(cli, ["mcp", "list"])
22
+ assert result.exit_code == 0
23
+ assert "notion" in result.output
24
+
25
+
26
+ def test_to_opencode_config_remote():
27
+ spec = {"transport": "http", "url": "https://example.com/mcp"}
28
+ assert to_opencode_config(spec) == {
29
+ "type": "remote",
30
+ "url": "https://example.com/mcp",
31
+ "enabled": True,
32
+ }
33
+
34
+
35
+ def test_to_opencode_config_local():
36
+ spec = {"transport": "stdio", "command": ["npx", "-y", "thing"]}
37
+ assert to_opencode_config(spec) == {
38
+ "type": "local",
39
+ "command": ["npx", "-y", "thing"],
40
+ "enabled": True,
41
+ }
42
+
43
+
44
+ def test_resolve_spec_from_registry():
45
+ spec = resolve_spec("notion", None, "http")
46
+ assert spec["url"] == "https://mcp.notion.com/mcp"
47
+
48
+
49
+ def test_resolve_spec_custom_url():
50
+ spec = resolve_spec("anything", "https://x.dev/mcp", "http")
51
+ assert spec["transport"] == "http"
52
+ assert spec["url"] == "https://x.dev/mcp"
53
+
54
+
55
+ def test_resolve_spec_unknown_raises():
56
+ with pytest.raises(Exception):
57
+ resolve_spec("nope", None, "http")
58
+
59
+
60
+ def test_add_opencode_only_writes_configs(runner, tmp_path, monkeypatch):
61
+ global_path = tmp_path / "global.jsonc"
62
+ monkeypatch.setattr(
63
+ "evo_cli.commands.mcp.get_global_config_path",
64
+ lambda: global_path,
65
+ )
66
+ project = tmp_path / "project"
67
+ project.mkdir()
68
+
69
+ result = runner.invoke(
70
+ cli,
71
+ ["mcp", "add", "notion", "--opencode-only", "--project", str(project)],
72
+ )
73
+ assert result.exit_code == 0
74
+ assert load_jsonc(global_path)["mcp"]["notion"]["url"] == "https://mcp.notion.com/mcp"
75
+ assert load_jsonc(project / "opencode.json")["mcp"]["notion"]["type"] == "remote"
76
+
77
+
78
+ def test_add_rejects_both_only_flags(runner, tmp_path, monkeypatch):
79
+ monkeypatch.setattr(
80
+ "evo_cli.commands.mcp.get_global_config_path",
81
+ lambda: tmp_path / "global.jsonc",
82
+ )
83
+ result = runner.invoke(
84
+ cli,
85
+ ["mcp", "add", "notion", "--claude-only", "--opencode-only"],
86
+ )
87
+ assert result.exit_code == 0
88
+ assert "mutually exclusive" in result.output
@@ -1 +0,0 @@
1
- 0.8.1
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes