meshcode 1.2.6__tar.gz → 1.3.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.
- {meshcode-1.2.6 → meshcode-1.3.0}/PKG-INFO +1 -1
- {meshcode-1.2.6 → meshcode-1.3.0}/meshcode/__init__.py +1 -1
- {meshcode-1.2.6 → meshcode-1.3.0}/meshcode/comms_v4.py +17 -9
- {meshcode-1.2.6 → meshcode-1.3.0}/meshcode/meshcode_mcp/server.py +31 -6
- meshcode-1.3.0/meshcode/run_agent.py +151 -0
- meshcode-1.3.0/meshcode/setup_clients.py +297 -0
- {meshcode-1.2.6 → meshcode-1.3.0}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-1.2.6 → meshcode-1.3.0}/meshcode.egg-info/SOURCES.txt +1 -0
- {meshcode-1.2.6 → meshcode-1.3.0}/pyproject.toml +1 -1
- meshcode-1.2.6/meshcode/setup_clients.py +0 -173
- {meshcode-1.2.6 → meshcode-1.3.0}/README.md +0 -0
- {meshcode-1.2.6 → meshcode-1.3.0}/meshcode/cli.py +0 -0
- {meshcode-1.2.6 → meshcode-1.3.0}/meshcode/launcher.py +0 -0
- {meshcode-1.2.6 → meshcode-1.3.0}/meshcode/launcher_install.py +0 -0
- {meshcode-1.2.6 → meshcode-1.3.0}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-1.2.6 → meshcode-1.3.0}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-1.2.6 → meshcode-1.3.0}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-1.2.6 → meshcode-1.3.0}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-1.2.6 → meshcode-1.3.0}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-1.2.6 → meshcode-1.3.0}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-1.2.6 → meshcode-1.3.0}/meshcode/protocol_v2.py +0 -0
- {meshcode-1.2.6 → meshcode-1.3.0}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-1.2.6 → meshcode-1.3.0}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-1.2.6 → meshcode-1.3.0}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-1.2.6 → meshcode-1.3.0}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-1.2.6 → meshcode-1.3.0}/setup.cfg +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""MeshCode — Real-time communication between AI agents."""
|
|
2
|
-
__version__ = "1.
|
|
2
|
+
__version__ = "1.3.0"
|
|
@@ -1888,17 +1888,25 @@ if __name__ == "__main__":
|
|
|
1888
1888
|
disconnect_terminal(proj, name)
|
|
1889
1889
|
|
|
1890
1890
|
elif cmd == "setup":
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1891
|
+
# Two forms supported:
|
|
1892
|
+
# meshcode setup <project> <agent> [role] → workspace flow (1.3+)
|
|
1893
|
+
# meshcode setup <client> <project> <agent> [role] → legacy global flow
|
|
1894
|
+
# Dispatched by setup_clients.setup() which detects which form was used.
|
|
1895
|
+
import importlib
|
|
1896
|
+
_setup_dispatcher = importlib.import_module("meshcode.setup_clients").setup
|
|
1897
|
+
sys.exit(_setup_dispatcher(*sys.argv[2:]))
|
|
1898
|
+
|
|
1899
|
+
elif cmd == "run":
|
|
1900
|
+
# meshcode run <agent> [--project <name>] [--editor claude|cursor|code]
|
|
1901
|
+
if len(sys.argv) < 3:
|
|
1902
|
+
print("Usage: meshcode run <agent> [--project <name>] [--editor claude|cursor|code]")
|
|
1894
1903
|
sys.exit(1)
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
role = sys.argv[5] if len(sys.argv) > 5 else ""
|
|
1904
|
+
agent = sys.argv[2]
|
|
1905
|
+
proj_override = flags.get("project")
|
|
1906
|
+
editor_override = flags.get("editor")
|
|
1899
1907
|
import importlib
|
|
1900
|
-
|
|
1901
|
-
sys.exit(
|
|
1908
|
+
_run = importlib.import_module("meshcode.run_agent").run
|
|
1909
|
+
sys.exit(_run(agent, project=proj_override, editor_override=editor_override))
|
|
1902
1910
|
|
|
1903
1911
|
elif cmd == "login":
|
|
1904
1912
|
key = sys.argv[2] if len(sys.argv) > 2 else ""
|
|
@@ -78,12 +78,37 @@ if isinstance(_register_result, dict) and _register_result.get("error"):
|
|
|
78
78
|
print(f"[meshcode-mcp] WARNING: register failed: {_register_result['error']}", file=sys.stderr)
|
|
79
79
|
|
|
80
80
|
# Flip to online so the dashboard reflects the live MCP session.
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
# Use the SECURITY DEFINER RPC (mc_agent_set_status_by_api_key) so we
|
|
82
|
+
# bypass RLS — the publishable key has no JWT context and cannot UPDATE
|
|
83
|
+
# mc_agents directly. The RPC validates ownership via api_key.
|
|
84
|
+
def _flip_status(status: str, task: str = "") -> bool:
|
|
85
|
+
api_key = os.environ.get("MESHCODE_API_KEY", "")
|
|
86
|
+
if not api_key:
|
|
87
|
+
# Last-resort fallback: try the direct PATCH (may be denied by RLS)
|
|
88
|
+
try:
|
|
89
|
+
be.set_status(_PROJECT_ID, AGENT_NAME, status, task)
|
|
90
|
+
return True
|
|
91
|
+
except Exception:
|
|
92
|
+
return False
|
|
93
|
+
try:
|
|
94
|
+
r = be.sb_rpc("mc_agent_set_status_by_api_key", {
|
|
95
|
+
"p_api_key": api_key,
|
|
96
|
+
"p_project_id": _PROJECT_ID,
|
|
97
|
+
"p_agent_name": AGENT_NAME,
|
|
98
|
+
"p_status": status,
|
|
99
|
+
"p_task": task,
|
|
100
|
+
})
|
|
101
|
+
if isinstance(r, dict) and r.get("ok"):
|
|
102
|
+
return True
|
|
103
|
+
if isinstance(r, dict) and r.get("error"):
|
|
104
|
+
log.warning(f"set_status RPC: {r['error']}")
|
|
105
|
+
return False
|
|
106
|
+
except Exception as e:
|
|
107
|
+
log.warning(f"set_status RPC threw: {e}")
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
if not _flip_status("online", "MCP session active"):
|
|
111
|
+
print(f"[meshcode-mcp] WARNING: could not flip status to online", file=sys.stderr)
|
|
87
112
|
|
|
88
113
|
|
|
89
114
|
# ============================================================
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""`meshcode run <agent>` — universal agent launcher.
|
|
2
|
+
|
|
3
|
+
Looks up the agent's workspace dir (created by `meshcode setup`) in the
|
|
4
|
+
registry at ~/meshcode/.registry.json, auto-detects the user's MCP-aware
|
|
5
|
+
editor, and launches it pointing at that workspace's isolated .mcp.json
|
|
6
|
+
so ONLY this one agent loads in the new window.
|
|
7
|
+
|
|
8
|
+
Editor detection order (first match wins):
|
|
9
|
+
1. $MESHCODE_EDITOR env var (explicit override: claude / cursor / code)
|
|
10
|
+
2. claude (Claude Code) — uses --mcp-config flag for per-instance config
|
|
11
|
+
3. cursor — opens Cursor in the workspace dir (reads .cursor/mcp.json)
|
|
12
|
+
4. code (VS Code, used by Cline) — opens VS Code in the workspace dir
|
|
13
|
+
(Cline reads .vscode/mcp.json)
|
|
14
|
+
|
|
15
|
+
The workspace dir contains all three config files (.mcp.json,
|
|
16
|
+
.cursor/mcp.json, .vscode/mcp.json) so any of these editors works
|
|
17
|
+
identically. Closing the editor window kills the MCP subprocess and
|
|
18
|
+
the agent flips to offline.
|
|
19
|
+
"""
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import shutil
|
|
23
|
+
import subprocess
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Optional, Tuple
|
|
27
|
+
|
|
28
|
+
WORKSPACES_ROOT = Path.home() / "meshcode"
|
|
29
|
+
REGISTRY_PATH = WORKSPACES_ROOT / ".registry.json"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _load_registry() -> dict:
|
|
33
|
+
if not REGISTRY_PATH.exists():
|
|
34
|
+
return {}
|
|
35
|
+
try:
|
|
36
|
+
return json.loads(REGISTRY_PATH.read_text())
|
|
37
|
+
except Exception:
|
|
38
|
+
return {}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _find_agent_workspace(agent: str, project: Optional[str] = None) -> Optional[Tuple[Path, str]]:
|
|
42
|
+
"""Look up the agent in the registry. Returns (workspace_path, project_name) or None.
|
|
43
|
+
|
|
44
|
+
If multiple agents share the same name across projects, requires the
|
|
45
|
+
user to disambiguate by passing project explicitly.
|
|
46
|
+
"""
|
|
47
|
+
reg = _load_registry()
|
|
48
|
+
agents = reg.get("agents", {})
|
|
49
|
+
|
|
50
|
+
# Direct hit by agent name (current model: agent names unique within registry)
|
|
51
|
+
info = agents.get(agent)
|
|
52
|
+
if info:
|
|
53
|
+
ws = Path(info["workspace"])
|
|
54
|
+
if not ws.exists():
|
|
55
|
+
print(f"[meshcode] ERROR: workspace dir for '{agent}' is missing: {ws}", file=sys.stderr)
|
|
56
|
+
print(f"[meshcode] Re-run: meshcode setup {info.get('project','<project>')} {agent}", file=sys.stderr)
|
|
57
|
+
return None
|
|
58
|
+
if project and info.get("project") != project:
|
|
59
|
+
print(f"[meshcode] ERROR: agent '{agent}' belongs to project '{info.get('project')}', not '{project}'", file=sys.stderr)
|
|
60
|
+
return None
|
|
61
|
+
return ws, info.get("project", "")
|
|
62
|
+
|
|
63
|
+
# Fallback: scan ~/meshcode for any dir matching <project>-<agent>
|
|
64
|
+
if project:
|
|
65
|
+
ws = WORKSPACES_ROOT / f"{project}-{agent}"
|
|
66
|
+
if ws.exists():
|
|
67
|
+
return ws, project
|
|
68
|
+
else:
|
|
69
|
+
# Scan all workspaces for any matching <*>-<agent>
|
|
70
|
+
if WORKSPACES_ROOT.exists():
|
|
71
|
+
matches = [p for p in WORKSPACES_ROOT.iterdir() if p.is_dir() and p.name.endswith(f"-{agent}")]
|
|
72
|
+
if len(matches) == 1:
|
|
73
|
+
return matches[0], matches[0].name.rsplit(f"-{agent}", 1)[0]
|
|
74
|
+
if len(matches) > 1:
|
|
75
|
+
print(f"[meshcode] ERROR: agent '{agent}' exists in multiple projects:", file=sys.stderr)
|
|
76
|
+
for m in matches:
|
|
77
|
+
print(f"[meshcode] {m.name}", file=sys.stderr)
|
|
78
|
+
print(f"[meshcode] Disambiguate: meshcode run {agent} --project <name>", file=sys.stderr)
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
print(f"[meshcode] ERROR: no workspace found for agent '{agent}'", file=sys.stderr)
|
|
82
|
+
print(f"[meshcode] Run `meshcode setup <project> {agent}` first.", file=sys.stderr)
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _detect_editor() -> Optional[str]:
|
|
87
|
+
"""Pick the user's preferred MCP-aware editor."""
|
|
88
|
+
override = os.environ.get("MESHCODE_EDITOR", "").strip().lower()
|
|
89
|
+
if override:
|
|
90
|
+
if shutil.which(override):
|
|
91
|
+
return override
|
|
92
|
+
print(f"[meshcode] WARNING: MESHCODE_EDITOR='{override}' not found in PATH", file=sys.stderr)
|
|
93
|
+
|
|
94
|
+
for cmd in ("claude", "cursor", "code"):
|
|
95
|
+
if shutil.which(cmd):
|
|
96
|
+
return cmd
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def run(agent: str, project: Optional[str] = None, editor_override: Optional[str] = None) -> int:
|
|
101
|
+
"""Launch the user's editor with ONLY the named agent's MCP server loaded."""
|
|
102
|
+
found = _find_agent_workspace(agent, project)
|
|
103
|
+
if not found:
|
|
104
|
+
return 2
|
|
105
|
+
ws, resolved_project = found
|
|
106
|
+
server_id = f"meshcode-{resolved_project}-{agent}"
|
|
107
|
+
|
|
108
|
+
editor = editor_override or _detect_editor()
|
|
109
|
+
if not editor:
|
|
110
|
+
print("[meshcode] ERROR: no MCP-aware editor found in PATH (claude / cursor / code).", file=sys.stderr)
|
|
111
|
+
print(f"[meshcode] Workspace is ready at: {ws}", file=sys.stderr)
|
|
112
|
+
print("[meshcode] Open it manually with the editor of your choice.", file=sys.stderr)
|
|
113
|
+
return 2
|
|
114
|
+
|
|
115
|
+
print(f"[meshcode] Launching {editor} for agent '{agent}' (project: {resolved_project})")
|
|
116
|
+
print(f"[meshcode] Workspace: {ws}")
|
|
117
|
+
print(f"[meshcode] MCP server: {server_id}")
|
|
118
|
+
print(f"[meshcode] Closing the editor will flip this agent offline.")
|
|
119
|
+
print()
|
|
120
|
+
|
|
121
|
+
if editor == "claude":
|
|
122
|
+
# Claude Code: pass --mcp-config to point at the workspace's .mcp.json
|
|
123
|
+
# and --strict-mcp-config so it ignores ~/.claude.json's mcpServers.
|
|
124
|
+
cmd = [
|
|
125
|
+
editor,
|
|
126
|
+
"--mcp-config", str(ws / ".mcp.json"),
|
|
127
|
+
"--strict-mcp-config",
|
|
128
|
+
"--dangerously-skip-permissions",
|
|
129
|
+
]
|
|
130
|
+
try:
|
|
131
|
+
os.chdir(ws)
|
|
132
|
+
except Exception:
|
|
133
|
+
pass
|
|
134
|
+
elif editor == "cursor":
|
|
135
|
+
# Cursor reads .cursor/mcp.json from the workspace cwd.
|
|
136
|
+
cmd = [editor, str(ws)]
|
|
137
|
+
elif editor == "code":
|
|
138
|
+
# VS Code (Cline reads .vscode/mcp.json from workspace cwd).
|
|
139
|
+
cmd = [editor, str(ws)]
|
|
140
|
+
else:
|
|
141
|
+
cmd = [editor, str(ws)]
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
# Replace this process with the editor so the user gets a clean tab.
|
|
145
|
+
os.execvp(cmd[0], cmd)
|
|
146
|
+
except FileNotFoundError:
|
|
147
|
+
print(f"[meshcode] ERROR: '{editor}' not found in PATH", file=sys.stderr)
|
|
148
|
+
return 127
|
|
149
|
+
except Exception as e:
|
|
150
|
+
print(f"[meshcode] ERROR launching {editor}: {e}", file=sys.stderr)
|
|
151
|
+
return 1
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""Per-agent workspace creator for `meshcode setup`.
|
|
2
|
+
|
|
3
|
+
NEW MODEL (1.3.0): instead of polluting the user's global MCP config
|
|
4
|
+
(~/.claude.json, ~/.cursor/mcp.json, ...), each `meshcode setup <project>
|
|
5
|
+
<agent>` creates an isolated workspace directory at:
|
|
6
|
+
|
|
7
|
+
~/meshcode/<project>-<agent>/
|
|
8
|
+
|
|
9
|
+
containing a single .mcp.json with ONLY that agent's MCP server entry.
|
|
10
|
+
The user then runs `meshcode run <agent>` to launch their editor with
|
|
11
|
+
that workspace, and ONLY that one agent goes online. Closing the editor
|
|
12
|
+
window flips the agent offline. No cross-window status pollution.
|
|
13
|
+
|
|
14
|
+
Backward compatibility: the old `meshcode setup <client> <project>
|
|
15
|
+
<agent>` form still works for the global-config flow (kept for users
|
|
16
|
+
on Claude Desktop, which doesn't support per-instance MCP configs).
|
|
17
|
+
"""
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import platform
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Dict, Any, Optional
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _load_credentials() -> Dict[str, str]:
|
|
27
|
+
creds_path = Path.home() / ".meshcode" / "credentials.json"
|
|
28
|
+
if not creds_path.exists():
|
|
29
|
+
print("[meshcode] ERROR: No credentials found. Run `meshcode login <api_key>` first.", file=sys.stderr)
|
|
30
|
+
sys.exit(2)
|
|
31
|
+
return json.loads(creds_path.read_text())
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _load_supabase_env() -> Dict[str, str]:
|
|
35
|
+
"""Read SUPABASE_URL/SUPABASE_KEY from env or ~/.meshcode/env, fall back to publishable defaults."""
|
|
36
|
+
url = os.environ.get("SUPABASE_URL", "")
|
|
37
|
+
key = os.environ.get("SUPABASE_KEY", "")
|
|
38
|
+
if not url or not key:
|
|
39
|
+
env_file = Path.home() / ".meshcode" / "env"
|
|
40
|
+
if env_file.exists():
|
|
41
|
+
for line in env_file.read_text().splitlines():
|
|
42
|
+
line = line.strip()
|
|
43
|
+
if line.startswith("export "):
|
|
44
|
+
line = line[7:]
|
|
45
|
+
if "=" in line:
|
|
46
|
+
k, v = line.split("=", 1)
|
|
47
|
+
v = v.strip().strip('"').strip("'")
|
|
48
|
+
if k == "SUPABASE_URL" and not url:
|
|
49
|
+
url = v
|
|
50
|
+
elif k == "SUPABASE_KEY" and not key:
|
|
51
|
+
key = v
|
|
52
|
+
if not url:
|
|
53
|
+
url = "https://wwgzzmydrwrjgaebspdo.supabase.co"
|
|
54
|
+
if not key:
|
|
55
|
+
key = "sb_publishable_0qf0U1GURopPIxLR8Vu7eQ_5grflPP4"
|
|
56
|
+
return {"SUPABASE_URL": url, "SUPABASE_KEY": key}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _resolve_project_id(api_key: str, project: str, sb: Dict[str, str]) -> str:
|
|
60
|
+
"""Call mc_resolve_project SECURITY DEFINER RPC to get the project_id."""
|
|
61
|
+
try:
|
|
62
|
+
from urllib.request import Request as _Req, urlopen as _urlopen
|
|
63
|
+
body = json.dumps({"p_api_key": api_key, "p_project_name": project}).encode()
|
|
64
|
+
req = _Req(
|
|
65
|
+
f"{sb['SUPABASE_URL']}/rest/v1/rpc/mc_resolve_project",
|
|
66
|
+
data=body,
|
|
67
|
+
method="POST",
|
|
68
|
+
headers={
|
|
69
|
+
"apikey": sb["SUPABASE_KEY"],
|
|
70
|
+
"Authorization": f"Bearer {sb['SUPABASE_KEY']}",
|
|
71
|
+
"Content-Type": "application/json",
|
|
72
|
+
},
|
|
73
|
+
)
|
|
74
|
+
with _urlopen(req, timeout=10) as resp:
|
|
75
|
+
data = json.loads(resp.read().decode())
|
|
76
|
+
if isinstance(data, dict) and data.get("project_id"):
|
|
77
|
+
return data["project_id"]
|
|
78
|
+
if isinstance(data, dict) and data.get("error"):
|
|
79
|
+
print(f"[meshcode] ERROR: could not resolve project '{project}': {data['error']}", file=sys.stderr)
|
|
80
|
+
sys.exit(2)
|
|
81
|
+
except Exception as e:
|
|
82
|
+
print(f"[meshcode] WARNING: project_id resolution failed ({e}); MCP server will retry at boot", file=sys.stderr)
|
|
83
|
+
return ""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _build_server_block(project: str, project_id: str, agent: str, role: str,
|
|
87
|
+
api_key: str, sb: Dict[str, str]) -> Dict[str, Any]:
|
|
88
|
+
return {
|
|
89
|
+
"command": sys.executable or "python3",
|
|
90
|
+
"args": ["-m", "meshcode.meshcode_mcp", "serve"],
|
|
91
|
+
"env": {
|
|
92
|
+
"MESHCODE_PROJECT": project,
|
|
93
|
+
"MESHCODE_PROJECT_ID": project_id,
|
|
94
|
+
"MESHCODE_AGENT": agent,
|
|
95
|
+
"MESHCODE_ROLE": role or "MCP-connected agent",
|
|
96
|
+
"MESHCODE_API_KEY": api_key,
|
|
97
|
+
"SUPABASE_URL": sb["SUPABASE_URL"],
|
|
98
|
+
"SUPABASE_KEY": sb["SUPABASE_KEY"],
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _atomic_write_json(path: Path, data: dict) -> None:
|
|
104
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
105
|
+
tmp = path.with_suffix(path.suffix + ".tmp")
|
|
106
|
+
tmp.write_text(json.dumps(data, indent=2))
|
|
107
|
+
tmp.replace(path)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# ============================================================
|
|
111
|
+
# WORKSPACE MODEL (1.3.0+) — one isolated dir per agent
|
|
112
|
+
# ============================================================
|
|
113
|
+
|
|
114
|
+
WORKSPACES_ROOT = Path.home() / "meshcode"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _workspace_dir(project: str, agent: str) -> Path:
|
|
118
|
+
return WORKSPACES_ROOT / f"{project}-{agent}"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def setup_workspace(project: str, agent: str, role: str = "") -> int:
|
|
122
|
+
"""Create an isolated workspace dir at ~/meshcode/<project>-<agent>/ with
|
|
123
|
+
.mcp.json containing ONLY this agent's server entry. Universal for any
|
|
124
|
+
MCP-compatible editor that supports per-directory configs (Claude Code via
|
|
125
|
+
--mcp-config flag, Cursor + Cline via .cursor/mcp.json + .vscode/mcp.json).
|
|
126
|
+
"""
|
|
127
|
+
creds = _load_credentials()
|
|
128
|
+
sb = _load_supabase_env()
|
|
129
|
+
api_key = creds.get("api_key", "")
|
|
130
|
+
project_id = _resolve_project_id(api_key, project, sb)
|
|
131
|
+
|
|
132
|
+
server_id = f"meshcode-{project}-{agent}"
|
|
133
|
+
server_block = _build_server_block(project, project_id, agent, role, api_key, sb)
|
|
134
|
+
|
|
135
|
+
ws = _workspace_dir(project, agent)
|
|
136
|
+
ws.mkdir(parents=True, exist_ok=True)
|
|
137
|
+
|
|
138
|
+
# Write 3 config files inside the workspace so any MCP-aware editor
|
|
139
|
+
# opened in this dir picks up the meshcode server.
|
|
140
|
+
#
|
|
141
|
+
# .mcp.json — Claude Code project-scoped MCP config
|
|
142
|
+
# .cursor/mcp.json — Cursor workspace-scoped MCP config
|
|
143
|
+
# .vscode/mcp.json — Cline / VS Code workspace-scoped MCP config
|
|
144
|
+
#
|
|
145
|
+
# All three contain the SAME single server entry. Whichever editor the
|
|
146
|
+
# user opens here will see exactly one agent: this one.
|
|
147
|
+
server_doc = {"mcpServers": {server_id: server_block}}
|
|
148
|
+
_atomic_write_json(ws / ".mcp.json", server_doc)
|
|
149
|
+
_atomic_write_json(ws / ".cursor" / "mcp.json", server_doc)
|
|
150
|
+
_atomic_write_json(ws / ".vscode" / "mcp.json", server_doc)
|
|
151
|
+
|
|
152
|
+
# A tiny manifest the `meshcode run` command reads to know where this
|
|
153
|
+
# workspace lives. Stored at the root so `meshcode run <agent>` can find
|
|
154
|
+
# it without scanning every dir.
|
|
155
|
+
registry_path = WORKSPACES_ROOT / ".registry.json"
|
|
156
|
+
registry: Dict[str, Any] = {}
|
|
157
|
+
if registry_path.exists():
|
|
158
|
+
try:
|
|
159
|
+
registry = json.loads(registry_path.read_text())
|
|
160
|
+
except Exception:
|
|
161
|
+
registry = {}
|
|
162
|
+
registry.setdefault("agents", {})[agent] = {
|
|
163
|
+
"project": project,
|
|
164
|
+
"workspace": str(ws),
|
|
165
|
+
"server_id": server_id,
|
|
166
|
+
"role": role,
|
|
167
|
+
}
|
|
168
|
+
_atomic_write_json(registry_path, registry)
|
|
169
|
+
|
|
170
|
+
print(f"[meshcode] ✓ Workspace created for agent '{agent}' (project: {project})")
|
|
171
|
+
print(f"[meshcode] Path: {ws}")
|
|
172
|
+
print(f"[meshcode]")
|
|
173
|
+
print(f"[meshcode] To launch this agent in your editor:")
|
|
174
|
+
print(f"[meshcode] meshcode run {agent}")
|
|
175
|
+
print(f"[meshcode]")
|
|
176
|
+
print(f"[meshcode] Closing the editor window flips this agent offline.")
|
|
177
|
+
print(f"[meshcode] Other agents stay independent — no cross-window pollution.")
|
|
178
|
+
return 0
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ============================================================
|
|
182
|
+
# LEGACY GLOBAL-CONFIG MODEL (kept for Claude Desktop)
|
|
183
|
+
# ============================================================
|
|
184
|
+
|
|
185
|
+
CLIENT_CONFIG_PATHS = {
|
|
186
|
+
"claude-code": {
|
|
187
|
+
"Darwin": Path.home() / ".claude.json",
|
|
188
|
+
"Linux": Path.home() / ".claude.json",
|
|
189
|
+
"Windows": Path.home() / ".claude.json",
|
|
190
|
+
},
|
|
191
|
+
"cursor": {
|
|
192
|
+
"Darwin": Path.home() / ".cursor" / "mcp.json",
|
|
193
|
+
"Linux": Path.home() / ".cursor" / "mcp.json",
|
|
194
|
+
"Windows": Path.home() / ".cursor" / "mcp.json",
|
|
195
|
+
},
|
|
196
|
+
"cline": {
|
|
197
|
+
"Darwin": Path.home() / "Library" / "Application Support" / "Code" / "User" / "globalStorage" / "saoudrizwan.claude-dev" / "settings" / "cline_mcp_settings.json",
|
|
198
|
+
"Linux": Path.home() / ".config" / "Code" / "User" / "globalStorage" / "saoudrizwan.claude-dev" / "settings" / "cline_mcp_settings.json",
|
|
199
|
+
"Windows": Path.home() / "AppData" / "Roaming" / "Code" / "User" / "globalStorage" / "saoudrizwan.claude-dev" / "settings" / "cline_mcp_settings.json",
|
|
200
|
+
},
|
|
201
|
+
"claude-desktop": {
|
|
202
|
+
"Darwin": Path.home() / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json",
|
|
203
|
+
"Linux": Path.home() / ".config" / "Claude" / "claude_desktop_config.json",
|
|
204
|
+
"Windows": Path.home() / "AppData" / "Roaming" / "Claude" / "claude_desktop_config.json",
|
|
205
|
+
},
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
CLIENT_DISPLAY_NAMES = {
|
|
209
|
+
"claude-code": "Claude Code",
|
|
210
|
+
"cursor": "Cursor",
|
|
211
|
+
"cline": "Cline (VS Code)",
|
|
212
|
+
"claude-desktop": "Claude Desktop",
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def setup_global(client: str, project: str, agent: str, role: str = "") -> int:
|
|
217
|
+
"""LEGACY: write to the user's GLOBAL MCP config file. All agents that
|
|
218
|
+
setup this way will load in EVERY window of the editor — they all show
|
|
219
|
+
online whenever any window is open. Kept for Claude Desktop and other
|
|
220
|
+
clients that don't support per-instance configs."""
|
|
221
|
+
if client not in CLIENT_CONFIG_PATHS:
|
|
222
|
+
print(f"[meshcode] ERROR: Unknown client '{client}'. Supported: {', '.join(CLIENT_CONFIG_PATHS)}", file=sys.stderr)
|
|
223
|
+
return 2
|
|
224
|
+
|
|
225
|
+
creds = _load_credentials()
|
|
226
|
+
sb = _load_supabase_env()
|
|
227
|
+
api_key = creds.get("api_key", "")
|
|
228
|
+
project_id = _resolve_project_id(api_key, project, sb)
|
|
229
|
+
|
|
230
|
+
os_name = platform.system()
|
|
231
|
+
config_path = CLIENT_CONFIG_PATHS[client].get(os_name)
|
|
232
|
+
if config_path is None:
|
|
233
|
+
print(f"[meshcode] ERROR: '{client}' is not supported on {os_name}", file=sys.stderr)
|
|
234
|
+
return 2
|
|
235
|
+
|
|
236
|
+
existing: Dict[str, Any] = {}
|
|
237
|
+
if config_path.exists():
|
|
238
|
+
try:
|
|
239
|
+
existing = json.loads(config_path.read_text())
|
|
240
|
+
except json.JSONDecodeError:
|
|
241
|
+
print(f"[meshcode] WARNING: {config_path} exists but is not valid JSON.", file=sys.stderr)
|
|
242
|
+
return 2
|
|
243
|
+
|
|
244
|
+
if not isinstance(existing, dict):
|
|
245
|
+
existing = {}
|
|
246
|
+
|
|
247
|
+
servers = existing.setdefault("mcpServers", {})
|
|
248
|
+
if not isinstance(servers, dict):
|
|
249
|
+
print(f"[meshcode] ERROR: 'mcpServers' in {config_path} is not an object", file=sys.stderr)
|
|
250
|
+
return 2
|
|
251
|
+
|
|
252
|
+
server_id = f"meshcode-{project}-{agent}"
|
|
253
|
+
servers[server_id] = _build_server_block(project, project_id, agent, role, api_key, sb)
|
|
254
|
+
_atomic_write_json(config_path, existing)
|
|
255
|
+
|
|
256
|
+
display = CLIENT_DISPLAY_NAMES[client]
|
|
257
|
+
print(f"[meshcode] MCP server '{server_id}' added to {display} GLOBAL config")
|
|
258
|
+
print(f"[meshcode] Path: {config_path}")
|
|
259
|
+
print(f"[meshcode] ⚠️ Global mode: this agent will appear in EVERY window of {display}.")
|
|
260
|
+
print(f"[meshcode] For per-window agent isolation, use the workspace flow instead:")
|
|
261
|
+
print(f"[meshcode] meshcode setup {project} {agent}")
|
|
262
|
+
print(f"[meshcode] meshcode run {agent}")
|
|
263
|
+
return 0
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# ============================================================
|
|
267
|
+
# DISPATCHER (called from comms_v4.py CLI)
|
|
268
|
+
# ============================================================
|
|
269
|
+
|
|
270
|
+
def setup(*args) -> int:
|
|
271
|
+
"""Smart dispatcher.
|
|
272
|
+
|
|
273
|
+
Forms supported:
|
|
274
|
+
meshcode setup <project> <agent> [role] → workspace flow (NEW)
|
|
275
|
+
meshcode setup <client> <project> <agent> [role] → legacy global flow
|
|
276
|
+
|
|
277
|
+
Detection: if the first arg is one of the known client slugs, fall back
|
|
278
|
+
to the legacy global flow. Otherwise treat the first arg as a project
|
|
279
|
+
name and create a workspace.
|
|
280
|
+
"""
|
|
281
|
+
if not args:
|
|
282
|
+
print("Usage:", file=sys.stderr)
|
|
283
|
+
print(" meshcode setup <project> <agent> [role] # workspace flow (recommended)", file=sys.stderr)
|
|
284
|
+
print(" meshcode setup <client> <project> <agent> [role] # legacy global flow", file=sys.stderr)
|
|
285
|
+
print(" Clients: claude-code, cursor, cline, claude-desktop", file=sys.stderr)
|
|
286
|
+
return 1
|
|
287
|
+
|
|
288
|
+
if args[0] in CLIENT_CONFIG_PATHS:
|
|
289
|
+
if len(args) < 3:
|
|
290
|
+
print("Usage: meshcode setup <client> <project> <agent> [role]", file=sys.stderr)
|
|
291
|
+
return 1
|
|
292
|
+
return setup_global(args[0], args[1], args[2], args[3] if len(args) > 3 else "")
|
|
293
|
+
|
|
294
|
+
if len(args) < 2:
|
|
295
|
+
print("Usage: meshcode setup <project> <agent> [role]", file=sys.stderr)
|
|
296
|
+
return 1
|
|
297
|
+
return setup_workspace(args[0], args[1], args[2] if len(args) > 2 else "")
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
"""Per-client MCP server config writers for `meshcode setup <client>`.
|
|
2
|
-
|
|
3
|
-
Supported clients: claude-code, cursor, cline, claude-desktop.
|
|
4
|
-
|
|
5
|
-
Each writer:
|
|
6
|
-
1. Resolves the right config file path for the current OS.
|
|
7
|
-
2. Loads existing JSON (or empty dict) and merges the meshcode MCP server entry
|
|
8
|
-
under "mcpServers". Never clobbers other entries.
|
|
9
|
-
3. Writes back atomically (temp file + rename).
|
|
10
|
-
4. Prints a success message with the path and "restart {client}" instructions.
|
|
11
|
-
"""
|
|
12
|
-
import json
|
|
13
|
-
import os
|
|
14
|
-
import platform
|
|
15
|
-
import sys
|
|
16
|
-
from pathlib import Path
|
|
17
|
-
from typing import Dict, Any
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def _load_credentials() -> Dict[str, str]:
|
|
21
|
-
creds_path = Path.home() / ".meshcode" / "credentials.json"
|
|
22
|
-
if not creds_path.exists():
|
|
23
|
-
print("[meshcode] ERROR: No credentials found. Run `meshcode login <api_key>` first.", file=sys.stderr)
|
|
24
|
-
sys.exit(2)
|
|
25
|
-
return json.loads(creds_path.read_text())
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def _load_supabase_env() -> Dict[str, str]:
|
|
29
|
-
"""Read SUPABASE_URL/SUPABASE_KEY from env or ~/.meshcode/env."""
|
|
30
|
-
url = os.environ.get("SUPABASE_URL", "")
|
|
31
|
-
key = os.environ.get("SUPABASE_KEY", "")
|
|
32
|
-
if not url or not key:
|
|
33
|
-
env_file = Path.home() / ".meshcode" / "env"
|
|
34
|
-
if env_file.exists():
|
|
35
|
-
for line in env_file.read_text().splitlines():
|
|
36
|
-
line = line.strip()
|
|
37
|
-
if line.startswith("export "):
|
|
38
|
-
line = line[7:]
|
|
39
|
-
if "=" in line:
|
|
40
|
-
k, v = line.split("=", 1)
|
|
41
|
-
v = v.strip().strip('"').strip("'")
|
|
42
|
-
if k == "SUPABASE_URL" and not url:
|
|
43
|
-
url = v
|
|
44
|
-
elif k == "SUPABASE_KEY" and not key:
|
|
45
|
-
key = v
|
|
46
|
-
if not url:
|
|
47
|
-
url = "https://wwgzzmydrwrjgaebspdo.supabase.co"
|
|
48
|
-
if not key:
|
|
49
|
-
key = "sb_publishable_0qf0U1GURopPIxLR8Vu7eQ_5grflPP4"
|
|
50
|
-
return {"SUPABASE_URL": url, "SUPABASE_KEY": key}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
CLIENT_CONFIG_PATHS = {
|
|
54
|
-
"claude-code": {
|
|
55
|
-
"Darwin": Path.home() / ".claude.json",
|
|
56
|
-
"Linux": Path.home() / ".claude.json",
|
|
57
|
-
"Windows": Path.home() / ".claude.json",
|
|
58
|
-
},
|
|
59
|
-
"cursor": {
|
|
60
|
-
"Darwin": Path.home() / ".cursor" / "mcp.json",
|
|
61
|
-
"Linux": Path.home() / ".cursor" / "mcp.json",
|
|
62
|
-
"Windows": Path.home() / ".cursor" / "mcp.json",
|
|
63
|
-
},
|
|
64
|
-
"cline": {
|
|
65
|
-
"Darwin": Path.home() / "Library" / "Application Support" / "Code" / "User" / "globalStorage" / "saoudrizwan.claude-dev" / "settings" / "cline_mcp_settings.json",
|
|
66
|
-
"Linux": Path.home() / ".config" / "Code" / "User" / "globalStorage" / "saoudrizwan.claude-dev" / "settings" / "cline_mcp_settings.json",
|
|
67
|
-
"Windows": Path.home() / "AppData" / "Roaming" / "Code" / "User" / "globalStorage" / "saoudrizwan.claude-dev" / "settings" / "cline_mcp_settings.json",
|
|
68
|
-
},
|
|
69
|
-
"claude-desktop": {
|
|
70
|
-
"Darwin": Path.home() / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json",
|
|
71
|
-
"Linux": Path.home() / ".config" / "Claude" / "claude_desktop_config.json",
|
|
72
|
-
"Windows": Path.home() / "AppData" / "Roaming" / "Claude" / "claude_desktop_config.json",
|
|
73
|
-
},
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
CLIENT_DISPLAY_NAMES = {
|
|
77
|
-
"claude-code": "Claude Code",
|
|
78
|
-
"cursor": "Cursor",
|
|
79
|
-
"cline": "Cline (VS Code)",
|
|
80
|
-
"claude-desktop": "Claude Desktop",
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def _atomic_write_json(path: Path, data: dict) -> None:
|
|
85
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
86
|
-
tmp = path.with_suffix(path.suffix + ".tmp")
|
|
87
|
-
tmp.write_text(json.dumps(data, indent=2))
|
|
88
|
-
tmp.replace(path)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def setup(client: str, project: str, agent: str, role: str = "") -> int:
|
|
92
|
-
"""Write MCP server config block for the given client. Returns 0 on success."""
|
|
93
|
-
if client not in CLIENT_CONFIG_PATHS:
|
|
94
|
-
print(f"[meshcode] ERROR: Unknown client '{client}'. Supported: {', '.join(CLIENT_CONFIG_PATHS)}", file=sys.stderr)
|
|
95
|
-
return 2
|
|
96
|
-
|
|
97
|
-
creds = _load_credentials()
|
|
98
|
-
sb = _load_supabase_env()
|
|
99
|
-
api_key = creds.get("api_key", "")
|
|
100
|
-
|
|
101
|
-
# Resolve project_id via the SECURITY DEFINER RPC so we don't depend on
|
|
102
|
-
# RLS letting the publishable key SELECT mc_projects at MCP server boot.
|
|
103
|
-
project_id = ""
|
|
104
|
-
try:
|
|
105
|
-
import json as _json
|
|
106
|
-
from urllib.request import Request as _Req, urlopen as _urlopen
|
|
107
|
-
_body = _json.dumps({"p_api_key": api_key, "p_project_name": project}).encode()
|
|
108
|
-
_req = _Req(
|
|
109
|
-
f"{sb['SUPABASE_URL']}/rest/v1/rpc/mc_resolve_project",
|
|
110
|
-
data=_body,
|
|
111
|
-
method="POST",
|
|
112
|
-
headers={
|
|
113
|
-
"apikey": sb["SUPABASE_KEY"],
|
|
114
|
-
"Authorization": f"Bearer {sb['SUPABASE_KEY']}",
|
|
115
|
-
"Content-Type": "application/json",
|
|
116
|
-
},
|
|
117
|
-
)
|
|
118
|
-
with _urlopen(_req, timeout=10) as _resp:
|
|
119
|
-
_data = _json.loads(_resp.read().decode())
|
|
120
|
-
if isinstance(_data, dict) and _data.get("project_id"):
|
|
121
|
-
project_id = _data["project_id"]
|
|
122
|
-
elif isinstance(_data, dict) and _data.get("error"):
|
|
123
|
-
print(f"[meshcode] ERROR: could not resolve project '{project}': {_data['error']}", file=sys.stderr)
|
|
124
|
-
return 2
|
|
125
|
-
except Exception as _e:
|
|
126
|
-
print(f"[meshcode] WARNING: project_id resolution failed ({_e}); MCP server will retry at boot", file=sys.stderr)
|
|
127
|
-
|
|
128
|
-
os_name = platform.system()
|
|
129
|
-
config_path = CLIENT_CONFIG_PATHS[client].get(os_name)
|
|
130
|
-
if config_path is None:
|
|
131
|
-
print(f"[meshcode] ERROR: '{client}' is not supported on {os_name}", file=sys.stderr)
|
|
132
|
-
return 2
|
|
133
|
-
|
|
134
|
-
existing: Dict[str, Any] = {}
|
|
135
|
-
if config_path.exists():
|
|
136
|
-
try:
|
|
137
|
-
existing = json.loads(config_path.read_text())
|
|
138
|
-
except json.JSONDecodeError:
|
|
139
|
-
print(f"[meshcode] WARNING: {config_path} exists but is not valid JSON. Overwriting may break {CLIENT_DISPLAY_NAMES[client]}.", file=sys.stderr)
|
|
140
|
-
return 2
|
|
141
|
-
|
|
142
|
-
if not isinstance(existing, dict):
|
|
143
|
-
existing = {}
|
|
144
|
-
|
|
145
|
-
servers = existing.setdefault("mcpServers", {})
|
|
146
|
-
if not isinstance(servers, dict):
|
|
147
|
-
print(f"[meshcode] ERROR: 'mcpServers' in {config_path} is not an object", file=sys.stderr)
|
|
148
|
-
return 2
|
|
149
|
-
|
|
150
|
-
server_id = f"meshcode-{project}-{agent}"
|
|
151
|
-
servers[server_id] = {
|
|
152
|
-
"command": sys.executable or "python3",
|
|
153
|
-
"args": ["-m", "meshcode.meshcode_mcp", "serve"],
|
|
154
|
-
"env": {
|
|
155
|
-
"MESHCODE_PROJECT": project,
|
|
156
|
-
"MESHCODE_PROJECT_ID": project_id,
|
|
157
|
-
"MESHCODE_AGENT": agent,
|
|
158
|
-
"MESHCODE_ROLE": role or "MCP-connected agent",
|
|
159
|
-
"MESHCODE_API_KEY": api_key,
|
|
160
|
-
"SUPABASE_URL": sb["SUPABASE_URL"],
|
|
161
|
-
"SUPABASE_KEY": sb["SUPABASE_KEY"],
|
|
162
|
-
},
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
_atomic_write_json(config_path, existing)
|
|
166
|
-
|
|
167
|
-
display = CLIENT_DISPLAY_NAMES[client]
|
|
168
|
-
print(f"[meshcode] MCP server '{server_id}' added to {display} config")
|
|
169
|
-
print(f"[meshcode] Path: {config_path}")
|
|
170
|
-
print(f"[meshcode] Restart {display} to load the server.")
|
|
171
|
-
print(f"[meshcode] Inside {display}, verify with /mcp — you should see '{server_id}' connected.")
|
|
172
|
-
print(f"[meshcode] Tools: meshcode_send, meshcode_check, meshcode_read, meshcode_status, meshcode_set_status, ...")
|
|
173
|
-
return 0
|
|
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
|