meshcode 1.2.6__tar.gz → 1.4.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.4.0}/PKG-INFO +1 -1
- {meshcode-1.2.6 → meshcode-1.4.0}/meshcode/__init__.py +1 -1
- {meshcode-1.2.6 → meshcode-1.4.0}/meshcode/comms_v4.py +17 -9
- {meshcode-1.2.6 → meshcode-1.4.0}/meshcode/meshcode_mcp/realtime.py +26 -0
- {meshcode-1.2.6 → meshcode-1.4.0}/meshcode/meshcode_mcp/server.py +299 -73
- meshcode-1.4.0/meshcode/run_agent.py +151 -0
- meshcode-1.4.0/meshcode/setup_clients.py +330 -0
- {meshcode-1.2.6 → meshcode-1.4.0}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-1.2.6 → meshcode-1.4.0}/meshcode.egg-info/SOURCES.txt +1 -0
- {meshcode-1.2.6 → meshcode-1.4.0}/pyproject.toml +1 -1
- meshcode-1.2.6/meshcode/setup_clients.py +0 -173
- {meshcode-1.2.6 → meshcode-1.4.0}/README.md +0 -0
- {meshcode-1.2.6 → meshcode-1.4.0}/meshcode/cli.py +0 -0
- {meshcode-1.2.6 → meshcode-1.4.0}/meshcode/launcher.py +0 -0
- {meshcode-1.2.6 → meshcode-1.4.0}/meshcode/launcher_install.py +0 -0
- {meshcode-1.2.6 → meshcode-1.4.0}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-1.2.6 → meshcode-1.4.0}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-1.2.6 → meshcode-1.4.0}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-1.2.6 → meshcode-1.4.0}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-1.2.6 → meshcode-1.4.0}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-1.2.6 → meshcode-1.4.0}/meshcode/protocol_v2.py +0 -0
- {meshcode-1.2.6 → meshcode-1.4.0}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-1.2.6 → meshcode-1.4.0}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-1.2.6 → meshcode-1.4.0}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-1.2.6 → meshcode-1.4.0}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-1.2.6 → meshcode-1.4.0}/setup.cfg +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""MeshCode — Real-time communication between AI agents."""
|
|
2
|
-
__version__ = "1.
|
|
2
|
+
__version__ = "1.4.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 ""
|
|
@@ -44,6 +44,9 @@ class RealtimeListener:
|
|
|
44
44
|
self._task: Optional[asyncio.Task] = None
|
|
45
45
|
self._stop = asyncio.Event()
|
|
46
46
|
self._connected = False
|
|
47
|
+
# Event fired whenever a new message is appended to the queue.
|
|
48
|
+
# meshcode_wait awaits this instead of polling → zero-cost idle.
|
|
49
|
+
self.message_event = asyncio.Event()
|
|
47
50
|
|
|
48
51
|
@property
|
|
49
52
|
def ws_url(self) -> str:
|
|
@@ -163,6 +166,11 @@ class RealtimeListener:
|
|
|
163
166
|
"id": record.get("id"),
|
|
164
167
|
}
|
|
165
168
|
self.queue.append(enriched)
|
|
169
|
+
# Wake any meshcode_wait blocked on this event.
|
|
170
|
+
try:
|
|
171
|
+
self.message_event.set()
|
|
172
|
+
except Exception:
|
|
173
|
+
pass
|
|
166
174
|
log.info(f"new message from {enriched['from']}")
|
|
167
175
|
if self.notify_callback:
|
|
168
176
|
try:
|
|
@@ -174,8 +182,26 @@ class RealtimeListener:
|
|
|
174
182
|
"""Pop and return all queued messages."""
|
|
175
183
|
out = list(self.queue)
|
|
176
184
|
self.queue.clear()
|
|
185
|
+
# Queue is empty → reset the wake event so the next wait blocks again.
|
|
186
|
+
try:
|
|
187
|
+
self.message_event.clear()
|
|
188
|
+
except Exception:
|
|
189
|
+
pass
|
|
177
190
|
return out
|
|
178
191
|
|
|
192
|
+
async def wait_for_message(self, timeout: Optional[float] = None) -> bool:
|
|
193
|
+
"""Block until a new message lands in the queue (or timeout).
|
|
194
|
+
|
|
195
|
+
Returns True if woken by a message, False on timeout. This is the
|
|
196
|
+
core of the zero-cost idle loop: while awaiting, the event loop
|
|
197
|
+
does ZERO work — no polling, no Supabase calls, no token cost.
|
|
198
|
+
"""
|
|
199
|
+
try:
|
|
200
|
+
await asyncio.wait_for(self.message_event.wait(), timeout=timeout)
|
|
201
|
+
return True
|
|
202
|
+
except asyncio.TimeoutError:
|
|
203
|
+
return False
|
|
204
|
+
|
|
179
205
|
@property
|
|
180
206
|
def is_connected(self) -> bool:
|
|
181
207
|
return self._connected
|
|
@@ -12,8 +12,75 @@ import json
|
|
|
12
12
|
import logging
|
|
13
13
|
import os
|
|
14
14
|
import sys
|
|
15
|
+
from collections import deque
|
|
15
16
|
from contextlib import asynccontextmanager
|
|
16
|
-
from typing import Any, Dict, List, Optional
|
|
17
|
+
from typing import Any, Dict, List, Optional, Union
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ============================================================
|
|
21
|
+
# Dedupe: track IDs of messages we've already returned from
|
|
22
|
+
# meshcode_wait / meshcode_check so the same row doesn't show
|
|
23
|
+
# up twice when realtime + polled paths race.
|
|
24
|
+
# ============================================================
|
|
25
|
+
_SEEN_MSG_IDS: set = set()
|
|
26
|
+
_SEEN_MSG_ORDER: deque = deque()
|
|
27
|
+
_SEEN_MSG_CAP = 1000
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _seen_key(msg: Dict[str, Any]) -> str:
|
|
31
|
+
"""Build a dedupe key. Prefer the real row id; fall back to a synthetic."""
|
|
32
|
+
mid = msg.get("id")
|
|
33
|
+
if mid:
|
|
34
|
+
return str(mid)
|
|
35
|
+
payload = msg.get("payload") or {}
|
|
36
|
+
try:
|
|
37
|
+
payload_str = json.dumps(payload, sort_keys=True, default=str)[:50]
|
|
38
|
+
except Exception:
|
|
39
|
+
payload_str = str(payload)[:50]
|
|
40
|
+
return f"{msg.get('from') or msg.get('from_agent')}|{msg.get('ts') or msg.get('created_at')}|{payload_str}"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _mark_seen(key: str) -> None:
|
|
44
|
+
if key in _SEEN_MSG_IDS:
|
|
45
|
+
return
|
|
46
|
+
_SEEN_MSG_IDS.add(key)
|
|
47
|
+
_SEEN_MSG_ORDER.append(key)
|
|
48
|
+
while len(_SEEN_MSG_ORDER) > _SEEN_MSG_CAP:
|
|
49
|
+
old = _SEEN_MSG_ORDER.popleft()
|
|
50
|
+
_SEEN_MSG_IDS.discard(old)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _filter_and_mark(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
54
|
+
"""Drop already-seen messages; mark the rest as seen."""
|
|
55
|
+
out = []
|
|
56
|
+
for m in messages:
|
|
57
|
+
k = _seen_key(m)
|
|
58
|
+
if k in _SEEN_MSG_IDS:
|
|
59
|
+
continue
|
|
60
|
+
_mark_seen(k)
|
|
61
|
+
out.append(m)
|
|
62
|
+
return out
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _split_messages(messages: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
66
|
+
"""Split a list of normalized message dicts into messages / acks / done_signals."""
|
|
67
|
+
real: List[Dict[str, Any]] = []
|
|
68
|
+
acks: List[Dict[str, Any]] = []
|
|
69
|
+
dones: List[Dict[str, Any]] = []
|
|
70
|
+
for m in messages:
|
|
71
|
+
t = m.get("type", "msg")
|
|
72
|
+
if t == "ack":
|
|
73
|
+
acks.append(m)
|
|
74
|
+
elif t == "done":
|
|
75
|
+
dones.append(m)
|
|
76
|
+
else:
|
|
77
|
+
real.append(m)
|
|
78
|
+
return {
|
|
79
|
+
"messages": real,
|
|
80
|
+
"acks": acks,
|
|
81
|
+
"done_signals": dones,
|
|
82
|
+
"count": len(real),
|
|
83
|
+
}
|
|
17
84
|
|
|
18
85
|
from . import backend as be
|
|
19
86
|
from .realtime import RealtimeListener
|
|
@@ -78,12 +145,85 @@ if isinstance(_register_result, dict) and _register_result.get("error"):
|
|
|
78
145
|
print(f"[meshcode-mcp] WARNING: register failed: {_register_result['error']}", file=sys.stderr)
|
|
79
146
|
|
|
80
147
|
# Flip to online so the dashboard reflects the live MCP session.
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
148
|
+
# Use the SECURITY DEFINER RPC (mc_agent_set_status_by_api_key) so we
|
|
149
|
+
# bypass RLS — the publishable key has no JWT context and cannot UPDATE
|
|
150
|
+
# mc_agents directly. The RPC validates ownership via api_key.
|
|
151
|
+
def _flip_status(status: str, task: str = "") -> bool:
|
|
152
|
+
api_key = os.environ.get("MESHCODE_API_KEY", "")
|
|
153
|
+
if not api_key:
|
|
154
|
+
# Last-resort fallback: try the direct PATCH (may be denied by RLS)
|
|
155
|
+
try:
|
|
156
|
+
be.set_status(_PROJECT_ID, AGENT_NAME, status, task)
|
|
157
|
+
return True
|
|
158
|
+
except Exception:
|
|
159
|
+
return False
|
|
160
|
+
try:
|
|
161
|
+
r = be.sb_rpc("mc_agent_set_status_by_api_key", {
|
|
162
|
+
"p_api_key": api_key,
|
|
163
|
+
"p_project_id": _PROJECT_ID,
|
|
164
|
+
"p_agent_name": AGENT_NAME,
|
|
165
|
+
"p_status": status,
|
|
166
|
+
"p_task": task,
|
|
167
|
+
})
|
|
168
|
+
if isinstance(r, dict) and r.get("ok"):
|
|
169
|
+
return True
|
|
170
|
+
if isinstance(r, dict) and r.get("error"):
|
|
171
|
+
log.warning(f"set_status RPC: {r['error']}")
|
|
172
|
+
return False
|
|
173
|
+
except Exception as e:
|
|
174
|
+
log.warning(f"set_status RPC threw: {e}")
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
if not _flip_status("online", "MCP session active"):
|
|
178
|
+
print(f"[meshcode-mcp] WARNING: could not flip status to online", file=sys.stderr)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ============================================================
|
|
182
|
+
# Single-instance lease — prevent split-brain when two windows
|
|
183
|
+
# run `meshcode run <same-agent>` simultaneously.
|
|
184
|
+
# ============================================================
|
|
185
|
+
import uuid as _uuid
|
|
186
|
+
_INSTANCE_ID = f"mcp-{_uuid.uuid4().hex[:12]}"
|
|
187
|
+
|
|
188
|
+
def _acquire_lease() -> bool:
|
|
189
|
+
api_key = os.environ.get("MESHCODE_API_KEY", "")
|
|
190
|
+
if not api_key:
|
|
191
|
+
return True # legacy clients without api_key skip lease check
|
|
192
|
+
try:
|
|
193
|
+
r = be.sb_rpc("mc_acquire_agent_lease", {
|
|
194
|
+
"p_api_key": api_key,
|
|
195
|
+
"p_project_id": _PROJECT_ID,
|
|
196
|
+
"p_agent_name": AGENT_NAME,
|
|
197
|
+
"p_instance_id": _INSTANCE_ID,
|
|
198
|
+
})
|
|
199
|
+
if isinstance(r, dict) and r.get("ok"):
|
|
200
|
+
return True
|
|
201
|
+
if isinstance(r, dict) and r.get("error"):
|
|
202
|
+
print(f"[meshcode-mcp] LEASE DENIED: {r['error']}", file=sys.stderr)
|
|
203
|
+
print(f"[meshcode-mcp] Another window is already running this agent.", file=sys.stderr)
|
|
204
|
+
print(f"[meshcode-mcp] Close the other window first, or use a different agent name.", file=sys.stderr)
|
|
205
|
+
return False
|
|
206
|
+
except Exception as e:
|
|
207
|
+
print(f"[meshcode-mcp] WARNING: lease RPC failed: {e}", file=sys.stderr)
|
|
208
|
+
return True # don't hard-fail on transient network issues
|
|
209
|
+
|
|
210
|
+
if not _acquire_lease():
|
|
211
|
+
sys.exit(2)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _release_lease() -> None:
|
|
215
|
+
api_key = os.environ.get("MESHCODE_API_KEY", "")
|
|
216
|
+
if not api_key:
|
|
217
|
+
return
|
|
218
|
+
try:
|
|
219
|
+
be.sb_rpc("mc_release_agent_lease", {
|
|
220
|
+
"p_api_key": api_key,
|
|
221
|
+
"p_project_id": _PROJECT_ID,
|
|
222
|
+
"p_agent_name": AGENT_NAME,
|
|
223
|
+
"p_instance_id": _INSTANCE_ID,
|
|
224
|
+
})
|
|
225
|
+
except Exception:
|
|
226
|
+
pass
|
|
87
227
|
|
|
88
228
|
|
|
89
229
|
# ============================================================
|
|
@@ -155,10 +295,19 @@ YOUR DEFAULT BEHAVIOR LOOP (do this without being asked):
|
|
|
155
295
|
user prompt. Read it, decide if it requires action or a reply, take
|
|
156
296
|
action, send a reply via meshcode_send if appropriate, then call
|
|
157
297
|
meshcode_wait AGAIN to keep listening.
|
|
158
|
-
5. When meshcode_wait returns timed_out
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
298
|
+
5. When meshcode_wait returns timed_out, call it AGAIN — but apply
|
|
299
|
+
TOKEN-EFFICIENT WAIT BACKOFF: double the previous timeout each
|
|
300
|
+
consecutive timeout (60 → 120 → 240 → 480 → 960 → 1800s, cap at
|
|
301
|
+
1800). Reset to 60s as soon as you receive ANY mesh message. Do
|
|
302
|
+
not return to the user. The loop is your default state — you are
|
|
303
|
+
a long-running mesh agent, not a one-shot chatbot.
|
|
304
|
+
|
|
305
|
+
6. GLOBAL DONE: when ANY agent (typically commander) calls
|
|
306
|
+
meshcode_done(reason), every other agent's meshcode_wait returns
|
|
307
|
+
with {got_done: true, reason}. Treat this as "task complete, exit
|
|
308
|
+
the loop". Do NOT call meshcode_wait again. Return cleanly to the
|
|
309
|
+
user with a summary of what the mesh accomplished. Stay quiet
|
|
310
|
+
until the user gives a new task.
|
|
162
311
|
|
|
163
312
|
YOU SHOULD ONLY BREAK OUT OF THE meshcode_wait LOOP IF:
|
|
164
313
|
- The human user explicitly says "stop" or pressed Ctrl+C.
|
|
@@ -188,6 +337,7 @@ YOUR FIRST ACTIONS WHEN THIS SESSION STARTS:
|
|
|
188
337
|
|
|
189
338
|
AVAILABLE MESH TOOLS (all from this MCP server):
|
|
190
339
|
meshcode_wait — block until a mesh message arrives (your idle state)
|
|
340
|
+
meshcode_done — broadcast a GLOBAL DONE to break every agent out of wait
|
|
191
341
|
meshcode_send — send a message to another mesh agent
|
|
192
342
|
meshcode_check — non-blocking peek at the inbox count
|
|
193
343
|
meshcode_read — drain inbox, mark messages read
|
|
@@ -257,13 +407,19 @@ async def lifespan(_app):
|
|
|
257
407
|
try:
|
|
258
408
|
yield {"realtime": _REALTIME}
|
|
259
409
|
finally:
|
|
260
|
-
log.info("lifespan shutdown — stopping heartbeat + realtime")
|
|
410
|
+
log.info("lifespan shutdown — stopping heartbeat + realtime + releasing lease")
|
|
261
411
|
hb_task.cancel()
|
|
262
412
|
try:
|
|
263
413
|
await hb_task
|
|
264
414
|
except asyncio.CancelledError:
|
|
265
415
|
pass
|
|
266
416
|
await _REALTIME.stop()
|
|
417
|
+
# Flip to offline + release lease so the dashboard reflects reality
|
|
418
|
+
# within seconds (not waiting for the 30s cron to notice).
|
|
419
|
+
try:
|
|
420
|
+
_release_lease()
|
|
421
|
+
except Exception as _e:
|
|
422
|
+
log.warning(f"could not release lease: {_e}")
|
|
267
423
|
|
|
268
424
|
|
|
269
425
|
# ============================================================
|
|
@@ -286,13 +442,24 @@ except Exception:
|
|
|
286
442
|
# ----------------- TOOLS -----------------
|
|
287
443
|
|
|
288
444
|
@mcp.tool()
|
|
289
|
-
def meshcode_send(to: str,
|
|
445
|
+
def meshcode_send(to: str, message: Any) -> Dict[str, Any]:
|
|
290
446
|
"""Send a message to another agent in the meshwork.
|
|
291
447
|
|
|
448
|
+
If you only have plain text, just pass it as a string and it will be
|
|
449
|
+
wrapped as {text: ...} automatically. For protocol payloads (need / done
|
|
450
|
+
/ fyi / blocked), pass a dict.
|
|
451
|
+
|
|
292
452
|
Args:
|
|
293
453
|
to: Name of the recipient agent.
|
|
294
|
-
|
|
454
|
+
message: Either a plain string (auto-wrapped as {"text": message}) or
|
|
455
|
+
a structured dict payload (use protocol keys: need/done/fyi/blocked).
|
|
295
456
|
"""
|
|
457
|
+
if isinstance(message, str):
|
|
458
|
+
payload: Dict[str, Any] = {"text": message}
|
|
459
|
+
elif isinstance(message, dict):
|
|
460
|
+
payload = message
|
|
461
|
+
else:
|
|
462
|
+
payload = {"text": str(message)}
|
|
296
463
|
return be.send_message(_PROJECT_ID, AGENT_NAME, to, payload, msg_type="msg")
|
|
297
464
|
|
|
298
465
|
|
|
@@ -314,84 +481,141 @@ def meshcode_broadcast(payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
314
481
|
|
|
315
482
|
@mcp.tool()
|
|
316
483
|
def meshcode_read() -> Dict[str, Any]:
|
|
317
|
-
"""Read all pending (unread) messages for this agent. Marks them as read and ACKs senders.
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
484
|
+
"""Read all pending (unread) messages for this agent. Marks them as read and ACKs senders.
|
|
485
|
+
|
|
486
|
+
Returns a split shape: `messages` (real msgs), `acks`, and `done_signals`
|
|
487
|
+
— so you don't have to filter by type yourself.
|
|
488
|
+
"""
|
|
489
|
+
raw = be.read_inbox(_PROJECT_ID, AGENT_NAME)
|
|
490
|
+
normalized = [
|
|
491
|
+
{
|
|
492
|
+
"from": m["from_agent"],
|
|
493
|
+
"type": m.get("type", "msg"),
|
|
494
|
+
"ts": m.get("created_at"),
|
|
495
|
+
"payload": m.get("payload", {}),
|
|
496
|
+
"id": m.get("id"),
|
|
497
|
+
}
|
|
498
|
+
for m in raw
|
|
499
|
+
]
|
|
500
|
+
deduped = _filter_and_mark(normalized)
|
|
501
|
+
return _split_messages(deduped)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _detect_global_done(messages: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
|
505
|
+
"""Return {reason, from} if the list contains a global_done signal, else None."""
|
|
506
|
+
for m in messages:
|
|
507
|
+
if m.get("type") == "done":
|
|
508
|
+
p = m.get("payload") or {}
|
|
509
|
+
if p.get("type") == "global_done" or p.get("reason"):
|
|
510
|
+
return {
|
|
511
|
+
"reason": p.get("reason"),
|
|
512
|
+
"from": p.get("from") or m.get("from"),
|
|
513
|
+
}
|
|
514
|
+
return None
|
|
331
515
|
|
|
332
516
|
|
|
333
517
|
@mcp.tool()
|
|
334
|
-
async def meshcode_wait(timeout_seconds: int =
|
|
518
|
+
async def meshcode_wait(timeout_seconds: int = 120) -> Dict[str, Any]:
|
|
335
519
|
"""LONG-POLL: Block until a new message arrives for this agent (or timeout).
|
|
336
520
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
521
|
+
This is a TRUE async long-poll. While idle, the MCP server blocks on a
|
|
522
|
+
single asyncio Event — zero CPU, zero Supabase calls, zero token cost on
|
|
523
|
+
the Claude side (the tool call literally has not returned yet). It wakes
|
|
524
|
+
INSTANTLY the moment a mesh message lands via the Realtime websocket.
|
|
525
|
+
|
|
526
|
+
TOKEN-EFFICIENT WAIT BACKOFF: when this returns timed_out, call it again
|
|
527
|
+
with double the previous timeout (60 → 120 → 240 → 480 → 960 → 1800s,
|
|
528
|
+
cap at 1800). Reset to 60s as soon as you receive ANY mesh message. This
|
|
529
|
+
minimizes the cost of being idle.
|
|
343
530
|
|
|
344
|
-
|
|
345
|
-
|
|
531
|
+
GLOBAL DONE: if any agent calls meshcode_done(reason), this tool returns
|
|
532
|
+
with {got_done: true, reason, from}. Treat that as "task complete, exit
|
|
533
|
+
the loop". Do NOT call meshcode_wait again after a got_done. Return
|
|
534
|
+
cleanly to the user with a summary and stay quiet until a new task.
|
|
346
535
|
|
|
347
536
|
Args:
|
|
348
|
-
timeout_seconds: How long to block before returning empty (default
|
|
537
|
+
timeout_seconds: How long to block before returning empty (default 120s).
|
|
349
538
|
Pick something < your client's tool-call timeout.
|
|
350
539
|
"""
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
if
|
|
540
|
+
actual_timeout = max(1, int(timeout_seconds))
|
|
541
|
+
|
|
542
|
+
def _return_from_buffered(buffered: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
|
543
|
+
deduped = _filter_and_mark(buffered)
|
|
544
|
+
if not deduped:
|
|
545
|
+
return None
|
|
546
|
+
split = _split_messages(deduped)
|
|
547
|
+
out: Dict[str, Any] = {
|
|
548
|
+
"got_message": True,
|
|
549
|
+
"source": "realtime",
|
|
550
|
+
**split,
|
|
551
|
+
}
|
|
552
|
+
done = _detect_global_done(deduped)
|
|
553
|
+
if done:
|
|
554
|
+
out["got_done"] = True
|
|
555
|
+
out["reason"] = done["reason"]
|
|
556
|
+
out["from"] = done["from"]
|
|
557
|
+
return out
|
|
558
|
+
|
|
559
|
+
# 1) Drain anything already buffered from before this call.
|
|
560
|
+
if _REALTIME:
|
|
561
|
+
pre = _REALTIME.drain()
|
|
562
|
+
if pre:
|
|
563
|
+
shaped = _return_from_buffered(pre)
|
|
564
|
+
if shaped:
|
|
565
|
+
return shaped
|
|
566
|
+
|
|
567
|
+
# 2) Real async wait — zero CPU, zero Supabase calls.
|
|
568
|
+
woke = await _REALTIME.wait_for_message(timeout=float(actual_timeout))
|
|
569
|
+
if woke:
|
|
356
570
|
buffered = _REALTIME.drain()
|
|
357
571
|
if buffered:
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
# 2) Safety net: poll Supabase directly in case realtime missed something
|
|
365
|
-
try:
|
|
366
|
-
pending_count = be.count_pending(_PROJECT_ID, AGENT_NAME)
|
|
367
|
-
except Exception:
|
|
368
|
-
pending_count = 0
|
|
369
|
-
if pending_count > 0:
|
|
370
|
-
messages = be.read_inbox(_PROJECT_ID, AGENT_NAME)
|
|
371
|
-
return {
|
|
372
|
-
"got_message": True,
|
|
373
|
-
"source": "polled",
|
|
374
|
-
"count": len(messages),
|
|
375
|
-
"messages": [
|
|
376
|
-
{
|
|
377
|
-
"from": m["from_agent"],
|
|
378
|
-
"type": m.get("type", "msg"),
|
|
379
|
-
"ts": m.get("created_at"),
|
|
380
|
-
"payload": m.get("payload", {}),
|
|
381
|
-
}
|
|
382
|
-
for m in messages
|
|
383
|
-
],
|
|
384
|
-
}
|
|
385
|
-
await asyncio.sleep(poll_interval)
|
|
572
|
+
shaped = _return_from_buffered(buffered)
|
|
573
|
+
if shaped:
|
|
574
|
+
return shaped
|
|
575
|
+
else:
|
|
576
|
+
# Realtime unavailable — plain sleep fallback so we still honor timeout.
|
|
577
|
+
await asyncio.sleep(actual_timeout)
|
|
386
578
|
|
|
387
579
|
return {
|
|
388
580
|
"got_message": False,
|
|
389
581
|
"timed_out": True,
|
|
390
582
|
"agent": AGENT_NAME,
|
|
391
|
-
"hint":
|
|
583
|
+
"hint": (
|
|
584
|
+
"No messages arrived. Per TOKEN-EFFICIENT WAIT BACKOFF: call "
|
|
585
|
+
"meshcode_wait again with DOUBLE the previous timeout "
|
|
586
|
+
"(60→120→240→480→960→1800, cap 1800). Reset to 60s on any "
|
|
587
|
+
"received mesh message."
|
|
588
|
+
),
|
|
392
589
|
}
|
|
393
590
|
|
|
394
591
|
|
|
592
|
+
@mcp.tool()
|
|
593
|
+
def meshcode_done(reason: str) -> Dict[str, Any]:
|
|
594
|
+
"""Broadcast a GLOBAL DONE signal to every other agent in the meshwork.
|
|
595
|
+
|
|
596
|
+
Typically called by the commander when the overall task is complete.
|
|
597
|
+
Every other agent's meshcode_wait() will return immediately with
|
|
598
|
+
{got_done: true, reason, from}, breaking them cleanly out of their idle
|
|
599
|
+
loop so they can report to the human and stop burning tokens.
|
|
600
|
+
|
|
601
|
+
Args:
|
|
602
|
+
reason: Short human-readable reason, e.g. "tests green, PR merged".
|
|
603
|
+
"""
|
|
604
|
+
agents = be.get_board(_PROJECT_ID)
|
|
605
|
+
payload = {"reason": reason, "from": AGENT_NAME, "type": "global_done"}
|
|
606
|
+
sent = 0
|
|
607
|
+
for a in agents:
|
|
608
|
+
name = a.get("name")
|
|
609
|
+
if name and name != AGENT_NAME:
|
|
610
|
+
try:
|
|
611
|
+
be.send_message(_PROJECT_ID, AGENT_NAME, name, payload, msg_type="done")
|
|
612
|
+
sent += 1
|
|
613
|
+
except Exception as e:
|
|
614
|
+
log.warning(f"meshcode_done: failed to notify {name}: {e}")
|
|
615
|
+
log.info(f"global done broadcast from {AGENT_NAME}: reason={reason!r} notified={sent}")
|
|
616
|
+
return {"ok": True, "global_done": True, "agents_notified": sent, "reason": reason}
|
|
617
|
+
|
|
618
|
+
|
|
395
619
|
@mcp.tool()
|
|
396
620
|
def meshcode_check() -> Dict[str, Any]:
|
|
397
621
|
"""Quick poll: returns pending message count + any messages buffered by the
|
|
@@ -402,12 +626,14 @@ def meshcode_check() -> Dict[str, Any]:
|
|
|
402
626
|
"""
|
|
403
627
|
pending = be.count_pending(_PROJECT_ID, AGENT_NAME)
|
|
404
628
|
realtime_buffered = _REALTIME.drain() if _REALTIME else []
|
|
629
|
+
deduped = _filter_and_mark(realtime_buffered)
|
|
630
|
+
split = _split_messages(deduped)
|
|
405
631
|
return {
|
|
406
632
|
"pending": pending,
|
|
407
633
|
"agent": AGENT_NAME,
|
|
408
634
|
"project": PROJECT_NAME,
|
|
409
635
|
"realtime_connected": _REALTIME.is_connected if _REALTIME else False,
|
|
410
|
-
|
|
636
|
+
**split,
|
|
411
637
|
}
|
|
412
638
|
|
|
413
639
|
|
|
@@ -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,330 @@
|
|
|
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
|
+
# Drop a README.md so the agent's working directory isn't empty on boot.
|
|
171
|
+
readme_body = f"""# {project} — {agent}
|
|
172
|
+
|
|
173
|
+
This is the MeshCode workspace for the **{agent}** agent in the **{project}** meshwork.
|
|
174
|
+
|
|
175
|
+
You are connected to the rest of your team via the meshcode MCP server, which is loaded
|
|
176
|
+
automatically when you open this directory in Claude Code, Cursor, or Cline.
|
|
177
|
+
|
|
178
|
+
## Your identity
|
|
179
|
+
- Project: {project}
|
|
180
|
+
- Agent name: {agent}
|
|
181
|
+
- Role: {role or "(set in dashboard)"}
|
|
182
|
+
|
|
183
|
+
## How this works
|
|
184
|
+
- Use the meshcode_* tools to communicate with other agents in the mesh.
|
|
185
|
+
- Use meshcode_status to see who else is in the meshwork.
|
|
186
|
+
- Use meshcode_send(to, message) to send messages.
|
|
187
|
+
- Use meshcode_wait to listen for incoming messages (your default idle state).
|
|
188
|
+
- Use meshcode_done(reason) when the team's task is complete.
|
|
189
|
+
|
|
190
|
+
## To launch this agent again
|
|
191
|
+
```bash
|
|
192
|
+
meshcode run {agent}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
This is your workspace dir — feel free to scaffold any files your task needs here, or
|
|
196
|
+
work in a different repo by `cd`ing elsewhere after launch.
|
|
197
|
+
"""
|
|
198
|
+
try:
|
|
199
|
+
(ws / "README.md").write_text(readme_body)
|
|
200
|
+
except Exception as _e:
|
|
201
|
+
print(f"[meshcode] WARNING: could not write README.md: {_e}", file=sys.stderr)
|
|
202
|
+
|
|
203
|
+
print(f"[meshcode] ✓ Workspace created for agent '{agent}' (project: {project})")
|
|
204
|
+
print(f"[meshcode] Path: {ws}")
|
|
205
|
+
print(f"[meshcode]")
|
|
206
|
+
print(f"[meshcode] To launch this agent in your editor:")
|
|
207
|
+
print(f"[meshcode] meshcode run {agent}")
|
|
208
|
+
print(f"[meshcode]")
|
|
209
|
+
print(f"[meshcode] Closing the editor window flips this agent offline.")
|
|
210
|
+
print(f"[meshcode] Other agents stay independent — no cross-window pollution.")
|
|
211
|
+
return 0
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# ============================================================
|
|
215
|
+
# LEGACY GLOBAL-CONFIG MODEL (kept for Claude Desktop)
|
|
216
|
+
# ============================================================
|
|
217
|
+
|
|
218
|
+
CLIENT_CONFIG_PATHS = {
|
|
219
|
+
"claude-code": {
|
|
220
|
+
"Darwin": Path.home() / ".claude.json",
|
|
221
|
+
"Linux": Path.home() / ".claude.json",
|
|
222
|
+
"Windows": Path.home() / ".claude.json",
|
|
223
|
+
},
|
|
224
|
+
"cursor": {
|
|
225
|
+
"Darwin": Path.home() / ".cursor" / "mcp.json",
|
|
226
|
+
"Linux": Path.home() / ".cursor" / "mcp.json",
|
|
227
|
+
"Windows": Path.home() / ".cursor" / "mcp.json",
|
|
228
|
+
},
|
|
229
|
+
"cline": {
|
|
230
|
+
"Darwin": Path.home() / "Library" / "Application Support" / "Code" / "User" / "globalStorage" / "saoudrizwan.claude-dev" / "settings" / "cline_mcp_settings.json",
|
|
231
|
+
"Linux": Path.home() / ".config" / "Code" / "User" / "globalStorage" / "saoudrizwan.claude-dev" / "settings" / "cline_mcp_settings.json",
|
|
232
|
+
"Windows": Path.home() / "AppData" / "Roaming" / "Code" / "User" / "globalStorage" / "saoudrizwan.claude-dev" / "settings" / "cline_mcp_settings.json",
|
|
233
|
+
},
|
|
234
|
+
"claude-desktop": {
|
|
235
|
+
"Darwin": Path.home() / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json",
|
|
236
|
+
"Linux": Path.home() / ".config" / "Claude" / "claude_desktop_config.json",
|
|
237
|
+
"Windows": Path.home() / "AppData" / "Roaming" / "Claude" / "claude_desktop_config.json",
|
|
238
|
+
},
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
CLIENT_DISPLAY_NAMES = {
|
|
242
|
+
"claude-code": "Claude Code",
|
|
243
|
+
"cursor": "Cursor",
|
|
244
|
+
"cline": "Cline (VS Code)",
|
|
245
|
+
"claude-desktop": "Claude Desktop",
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def setup_global(client: str, project: str, agent: str, role: str = "") -> int:
|
|
250
|
+
"""LEGACY: write to the user's GLOBAL MCP config file. All agents that
|
|
251
|
+
setup this way will load in EVERY window of the editor — they all show
|
|
252
|
+
online whenever any window is open. Kept for Claude Desktop and other
|
|
253
|
+
clients that don't support per-instance configs."""
|
|
254
|
+
if client not in CLIENT_CONFIG_PATHS:
|
|
255
|
+
print(f"[meshcode] ERROR: Unknown client '{client}'. Supported: {', '.join(CLIENT_CONFIG_PATHS)}", file=sys.stderr)
|
|
256
|
+
return 2
|
|
257
|
+
|
|
258
|
+
creds = _load_credentials()
|
|
259
|
+
sb = _load_supabase_env()
|
|
260
|
+
api_key = creds.get("api_key", "")
|
|
261
|
+
project_id = _resolve_project_id(api_key, project, sb)
|
|
262
|
+
|
|
263
|
+
os_name = platform.system()
|
|
264
|
+
config_path = CLIENT_CONFIG_PATHS[client].get(os_name)
|
|
265
|
+
if config_path is None:
|
|
266
|
+
print(f"[meshcode] ERROR: '{client}' is not supported on {os_name}", file=sys.stderr)
|
|
267
|
+
return 2
|
|
268
|
+
|
|
269
|
+
existing: Dict[str, Any] = {}
|
|
270
|
+
if config_path.exists():
|
|
271
|
+
try:
|
|
272
|
+
existing = json.loads(config_path.read_text())
|
|
273
|
+
except json.JSONDecodeError:
|
|
274
|
+
print(f"[meshcode] WARNING: {config_path} exists but is not valid JSON.", file=sys.stderr)
|
|
275
|
+
return 2
|
|
276
|
+
|
|
277
|
+
if not isinstance(existing, dict):
|
|
278
|
+
existing = {}
|
|
279
|
+
|
|
280
|
+
servers = existing.setdefault("mcpServers", {})
|
|
281
|
+
if not isinstance(servers, dict):
|
|
282
|
+
print(f"[meshcode] ERROR: 'mcpServers' in {config_path} is not an object", file=sys.stderr)
|
|
283
|
+
return 2
|
|
284
|
+
|
|
285
|
+
server_id = f"meshcode-{project}-{agent}"
|
|
286
|
+
servers[server_id] = _build_server_block(project, project_id, agent, role, api_key, sb)
|
|
287
|
+
_atomic_write_json(config_path, existing)
|
|
288
|
+
|
|
289
|
+
display = CLIENT_DISPLAY_NAMES[client]
|
|
290
|
+
print(f"[meshcode] MCP server '{server_id}' added to {display} GLOBAL config")
|
|
291
|
+
print(f"[meshcode] Path: {config_path}")
|
|
292
|
+
print(f"[meshcode] ⚠️ Global mode: this agent will appear in EVERY window of {display}.")
|
|
293
|
+
print(f"[meshcode] For per-window agent isolation, use the workspace flow instead:")
|
|
294
|
+
print(f"[meshcode] meshcode setup {project} {agent}")
|
|
295
|
+
print(f"[meshcode] meshcode run {agent}")
|
|
296
|
+
return 0
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# ============================================================
|
|
300
|
+
# DISPATCHER (called from comms_v4.py CLI)
|
|
301
|
+
# ============================================================
|
|
302
|
+
|
|
303
|
+
def setup(*args) -> int:
|
|
304
|
+
"""Smart dispatcher.
|
|
305
|
+
|
|
306
|
+
Forms supported:
|
|
307
|
+
meshcode setup <project> <agent> [role] → workspace flow (NEW)
|
|
308
|
+
meshcode setup <client> <project> <agent> [role] → legacy global flow
|
|
309
|
+
|
|
310
|
+
Detection: if the first arg is one of the known client slugs, fall back
|
|
311
|
+
to the legacy global flow. Otherwise treat the first arg as a project
|
|
312
|
+
name and create a workspace.
|
|
313
|
+
"""
|
|
314
|
+
if not args:
|
|
315
|
+
print("Usage:", file=sys.stderr)
|
|
316
|
+
print(" meshcode setup <project> <agent> [role] # workspace flow (recommended)", file=sys.stderr)
|
|
317
|
+
print(" meshcode setup <client> <project> <agent> [role] # legacy global flow", file=sys.stderr)
|
|
318
|
+
print(" Clients: claude-code, cursor, cline, claude-desktop", file=sys.stderr)
|
|
319
|
+
return 1
|
|
320
|
+
|
|
321
|
+
if args[0] in CLIENT_CONFIG_PATHS:
|
|
322
|
+
if len(args) < 3:
|
|
323
|
+
print("Usage: meshcode setup <client> <project> <agent> [role]", file=sys.stderr)
|
|
324
|
+
return 1
|
|
325
|
+
return setup_global(args[0], args[1], args[2], args[3] if len(args) > 3 else "")
|
|
326
|
+
|
|
327
|
+
if len(args) < 2:
|
|
328
|
+
print("Usage: meshcode setup <project> <agent> [role]", file=sys.stderr)
|
|
329
|
+
return 1
|
|
330
|
+
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
|