meshcode 2.1.1__tar.gz → 2.2.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-2.1.1 → meshcode-2.2.0}/PKG-INFO +1 -1
- {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/__init__.py +1 -1
- {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/meshcode_mcp/server.py +62 -145
- meshcode-2.2.0/meshcode/meshcode_mcp/test_server_wrapper.py +117 -0
- {meshcode-2.1.1 → meshcode-2.2.0}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.1.1 → meshcode-2.2.0}/meshcode.egg-info/SOURCES.txt +2 -1
- {meshcode-2.1.1 → meshcode-2.2.0}/pyproject.toml +1 -1
- {meshcode-2.1.1 → meshcode-2.2.0}/README.md +0 -0
- {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/cli.py +0 -0
- {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/comms_v4.py +0 -0
- {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/invites.py +0 -0
- {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/launcher.py +0 -0
- {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/launcher_install.py +0 -0
- {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/preferences.py +0 -0
- {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/run_agent.py +0 -0
- {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/secrets.py +0 -0
- {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/self_update.py +0 -0
- {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/setup_clients.py +0 -0
- {meshcode-2.1.1 → meshcode-2.2.0}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.1.1 → meshcode-2.2.0}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.1.1 → meshcode-2.2.0}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.1.1 → meshcode-2.2.0}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.1.1 → meshcode-2.2.0}/setup.cfg +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""MeshCode — Real-time communication between AI agents."""
|
|
2
|
-
__version__ = "2.
|
|
2
|
+
__version__ = "2.2.0"
|
|
@@ -289,6 +289,15 @@ if not _PROJECT_ID:
|
|
|
289
289
|
print(f"[meshcode-mcp] ERROR: project '{PROJECT_NAME}' not found (check MESHCODE_KEYCHAIN_PROFILE / MESHCODE_API_KEY)", file=sys.stderr)
|
|
290
290
|
sys.exit(2)
|
|
291
291
|
|
|
292
|
+
# Resolve project plan for adaptive features (heartbeat interval, etc.)
|
|
293
|
+
_PROJECT_PLAN = "free"
|
|
294
|
+
try:
|
|
295
|
+
_plan_rows = be.sb_select("mc_projects", f"id=eq.{_PROJECT_ID}", limit=1)
|
|
296
|
+
if _plan_rows and isinstance(_plan_rows, list) and len(_plan_rows) > 0:
|
|
297
|
+
_PROJECT_PLAN = _plan_rows[0].get("plan", "free") or "free"
|
|
298
|
+
except Exception:
|
|
299
|
+
pass # default to free
|
|
300
|
+
|
|
292
301
|
_register_result = be.register_agent(PROJECT_NAME, AGENT_NAME, AGENT_ROLE or "MCP-connected agent")
|
|
293
302
|
if isinstance(_register_result, dict) and _register_result.get("error"):
|
|
294
303
|
print(f"[meshcode-mcp] WARNING: register failed: {_register_result['error']}", file=sys.stderr)
|
|
@@ -342,6 +351,7 @@ _current_state = "online"
|
|
|
342
351
|
_last_tool_at = _time.time()
|
|
343
352
|
_current_tool = ""
|
|
344
353
|
_IDLE_THRESHOLD_S = 120 # seconds without tool call → IDLE
|
|
354
|
+
_SLEEPING_THRESHOLD_S = 300 # seconds in waiting without activity → SLEEPING
|
|
345
355
|
_WORKING_COOLDOWN_S = 60 # seconds after last tool returns before flipping to ONLINE
|
|
346
356
|
_working_timer: Optional[_threading.Timer] = None
|
|
347
357
|
|
|
@@ -422,12 +432,10 @@ def with_working_status(func):
|
|
|
422
432
|
return func(*args, **kwargs)
|
|
423
433
|
finally:
|
|
424
434
|
if not skip:
|
|
425
|
-
global _last_tool_at
|
|
435
|
+
global _last_tool_at
|
|
426
436
|
_last_tool_at = _time.time()
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
_processing_timer.daemon = True
|
|
430
|
-
_processing_timer.start()
|
|
437
|
+
# Don't flip to online here — CPU-based detection in heartbeat
|
|
438
|
+
# will handle the transition when LLM stops generating
|
|
431
439
|
return swrapper
|
|
432
440
|
|
|
433
441
|
|
|
@@ -565,23 +573,21 @@ def _build_instructions() -> str:
|
|
|
565
573
|
)
|
|
566
574
|
base = f"""You are agent "{AGENT_NAME}" in meshwork "{PROJECT_NAME}".{role_block}{launch_block}
|
|
567
575
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
576
|
+
PERMANENT LOOP (MANDATORY — #1 RULE):
|
|
577
|
+
You MUST stay in meshcode_wait() forever. After EVERY action, call meshcode_wait().
|
|
578
|
+
NEVER exit. NEVER stop. NEVER say "standing by" without calling meshcode_wait().
|
|
579
|
+
The ONLY exits: user says "stop", got_done signal, or fatal error.
|
|
580
|
+
Timeout? Call meshcode_wait() again with 2x timeout (cap 1800s).
|
|
581
|
+
|
|
582
|
+
LOOP: act → meshcode_send if needed → meshcode_wait() → repeat.
|
|
573
583
|
|
|
574
584
|
RULES:
|
|
575
|
-
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
-
|
|
579
|
-
-
|
|
580
|
-
-
|
|
581
|
-
- Poll meshcode_tasks() before each meshcode_wait().
|
|
582
|
-
- Threading: pass in_reply_to with original msg id.
|
|
583
|
-
- sensitive=True for credentials/secrets/PII in meshcode_send.
|
|
584
|
-
- No feedback loops: stop if >10 messages on same topic.
|
|
585
|
+
- Use MCP tools only (never CLI commands in bash).
|
|
586
|
+
- Tasks > messages. Claim tasks via meshcode_tasks/task_claim/task_complete.
|
|
587
|
+
- Messages <100 tokens. Long content → create task.
|
|
588
|
+
- No empty acks. JSON reports only.
|
|
589
|
+
- Threading: pass in_reply_to.
|
|
590
|
+
- sensitive=True for secrets/PII.
|
|
585
591
|
|
|
586
592
|
SESSION START:
|
|
587
593
|
1. meshcode_set_status(status="online", task="ready") — announce you're online
|
|
@@ -716,18 +722,22 @@ def _heartbeat_thread_fn():
|
|
|
716
722
|
try:
|
|
717
723
|
be.sb_rpc("mc_heartbeat", {"p_project_id": _PROJECT_ID, "p_agent_name": AGENT_NAME, "p_version": "2.0.0"})
|
|
718
724
|
|
|
719
|
-
# CPU-based status detection
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
_set_state("
|
|
729
|
-
|
|
730
|
-
|
|
725
|
+
# CPU-based status detection
|
|
726
|
+
parent_cpu = _get_parent_cpu()
|
|
727
|
+
idle_secs = _time.time() - _last_tool_at
|
|
728
|
+
|
|
729
|
+
if _current_state == "waiting":
|
|
730
|
+
pass # In meshcode_wait loop — NEVER override. Agent is listening.
|
|
731
|
+
elif parent_cpu > 5.0:
|
|
732
|
+
# LLM is actively generating tokens
|
|
733
|
+
if _current_state != "working":
|
|
734
|
+
_set_state("working", "generating response")
|
|
735
|
+
elif _current_state == "working":
|
|
736
|
+
# LLM just stopped — brief online before sleeping
|
|
737
|
+
_set_state("online", "")
|
|
738
|
+
elif _current_state in ("online", "idle") and idle_secs > 30:
|
|
739
|
+
# Not in loop, not thinking → sleeping
|
|
740
|
+
_set_state("sleeping", "sleeping")
|
|
731
741
|
|
|
732
742
|
# Sync current state to DB (in case realtime missed it)
|
|
733
743
|
try:
|
|
@@ -755,7 +765,10 @@ def _heartbeat_thread_fn():
|
|
|
755
765
|
except Exception as e:
|
|
756
766
|
log.warning(f"lease renewal failed: {e}")
|
|
757
767
|
|
|
758
|
-
|
|
768
|
+
# Adaptive heartbeat: fast for paid plans, moderate for free
|
|
769
|
+
# Free: 15s (~6K req/day/agent). Pro+: 5s (~17K req/day/agent).
|
|
770
|
+
hb_interval = 5 if _PROJECT_PLAN in ("pro", "team", "enterprise", "unlimited") else 15
|
|
771
|
+
_heartbeat_stop.wait(hb_interval)
|
|
759
772
|
|
|
760
773
|
|
|
761
774
|
@asynccontextmanager
|
|
@@ -833,14 +846,7 @@ except Exception:
|
|
|
833
846
|
@with_working_status
|
|
834
847
|
def meshcode_send(to: str, message: Any, in_reply_to: Optional[str] = None,
|
|
835
848
|
sensitive: bool = False) -> Dict[str, Any]:
|
|
836
|
-
"""Send
|
|
837
|
-
|
|
838
|
-
Args:
|
|
839
|
-
to: Recipient agent name (or agent@meshwork for cross-mesh).
|
|
840
|
-
message: String or dict payload.
|
|
841
|
-
in_reply_to: Original message id for threading.
|
|
842
|
-
sensitive: True to hide from public exports.
|
|
843
|
-
"""
|
|
849
|
+
"""Send message. Use "agent@meshwork" for cross-mesh. sensitive=True hides from exports."""
|
|
844
850
|
if isinstance(message, str):
|
|
845
851
|
payload: Dict[str, Any] = {"text": message}
|
|
846
852
|
elif isinstance(message, dict):
|
|
@@ -884,16 +890,7 @@ def meshcode_broadcast(payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
884
890
|
@mcp.tool()
|
|
885
891
|
@with_working_status
|
|
886
892
|
def meshcode_read(include_acks: bool = False) -> Dict[str, Any]:
|
|
887
|
-
"""Read and
|
|
888
|
-
|
|
889
|
-
Returns a split shape: `messages` (real msgs), `acks`, and `done_signals`
|
|
890
|
-
— so you don't have to filter by type yourself.
|
|
891
|
-
|
|
892
|
-
Args:
|
|
893
|
-
include_acks: If False (default), type='ack' rows are dropped entirely
|
|
894
|
-
from the response. Acks are bookkeeping noise and most callers
|
|
895
|
-
don't want them in their LLM context.
|
|
896
|
-
"""
|
|
893
|
+
"""Read and consume pending messages. Returns {messages, acks, done_signals}."""
|
|
897
894
|
raw = be.read_inbox(_PROJECT_ID, AGENT_NAME)
|
|
898
895
|
normalized = [
|
|
899
896
|
{
|
|
@@ -916,13 +913,7 @@ def meshcode_read(include_acks: bool = False) -> Dict[str, Any]:
|
|
|
916
913
|
@mcp.tool()
|
|
917
914
|
@with_working_status
|
|
918
915
|
def meshcode_history(limit: int = 20, agent_filter: Optional[str] = None) -> Dict[str, Any]:
|
|
919
|
-
"""View
|
|
920
|
-
context from past conversations or lost messages after context compression.
|
|
921
|
-
|
|
922
|
-
Args:
|
|
923
|
-
limit: Max messages to return (default 20).
|
|
924
|
-
agent_filter: Optional agent name to filter (shows messages to/from that agent).
|
|
925
|
-
"""
|
|
916
|
+
"""View past messages (read+unread). Optional agent_filter."""
|
|
926
917
|
raw = be.get_history(_PROJECT_ID, limit=limit, agent_filter=agent_filter or "")
|
|
927
918
|
messages = [
|
|
928
919
|
{
|
|
@@ -942,12 +933,7 @@ def meshcode_history(limit: int = 20, agent_filter: Optional[str] = None) -> Dic
|
|
|
942
933
|
@mcp.tool()
|
|
943
934
|
@with_working_status
|
|
944
935
|
def meshcode_read_message(msg_id: str) -> Dict[str, Any]:
|
|
945
|
-
"""Fetch a specific message by
|
|
946
|
-
after context was compressed or session restarted.
|
|
947
|
-
|
|
948
|
-
Args:
|
|
949
|
-
msg_id: The UUID of the message to fetch.
|
|
950
|
-
"""
|
|
936
|
+
"""Fetch a specific message by its UUID."""
|
|
951
937
|
msg = be.get_message_by_id(_PROJECT_ID, msg_id)
|
|
952
938
|
if not msg:
|
|
953
939
|
return {"error": "message not found", "msg_id": msg_id}
|
|
@@ -1043,20 +1029,7 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
|
|
|
1043
1029
|
# Realtime unavailable — plain sleep fallback so we still honor timeout.
|
|
1044
1030
|
await asyncio.sleep(actual_timeout)
|
|
1045
1031
|
|
|
1046
|
-
|
|
1047
|
-
timeout_result: Dict[str, Any] = {"timed_out": True}
|
|
1048
|
-
try:
|
|
1049
|
-
api_key = _get_api_key()
|
|
1050
|
-
open_tasks = be.task_list(api_key, _PROJECT_ID, AGENT_NAME, status_filter="open")
|
|
1051
|
-
if isinstance(open_tasks, dict) and open_tasks.get("ok"):
|
|
1052
|
-
my_tasks = [t for t in open_tasks.get("tasks", [])
|
|
1053
|
-
if t.get("assignee") == AGENT_NAME and not t.get("claimed_by")]
|
|
1054
|
-
if my_tasks:
|
|
1055
|
-
timeout_result["unclaimed_tasks"] = len(my_tasks)
|
|
1056
|
-
timeout_result["hint"] = f"You have {len(my_tasks)} unclaimed task(s) assigned to you. Run meshcode_tasks(status_filter='open') to see them."
|
|
1057
|
-
except Exception:
|
|
1058
|
-
pass
|
|
1059
|
-
return timeout_result
|
|
1032
|
+
return {"timed_out": True}
|
|
1060
1033
|
|
|
1061
1034
|
|
|
1062
1035
|
@mcp.tool()
|
|
@@ -1085,12 +1058,7 @@ def meshcode_done(reason: str) -> Dict[str, Any]:
|
|
|
1085
1058
|
@mcp.tool()
|
|
1086
1059
|
@with_working_status
|
|
1087
1060
|
def meshcode_check(include_acks: bool = False) -> Dict[str, Any]:
|
|
1088
|
-
"""
|
|
1089
|
-
|
|
1090
|
-
Checks realtime buffer first, then falls back to DB if buffer is empty
|
|
1091
|
-
but there are pending messages (handles messages that arrived before
|
|
1092
|
-
the realtime listener connected).
|
|
1093
|
-
"""
|
|
1061
|
+
"""Peek at inbox (non-destructive). Returns pending count + new messages."""
|
|
1094
1062
|
pending = be.count_pending(_PROJECT_ID, AGENT_NAME)
|
|
1095
1063
|
# Peek at realtime buffer WITHOUT draining — check is non-destructive
|
|
1096
1064
|
realtime_buffered = _REALTIME.peek() if _REALTIME else []
|
|
@@ -1151,9 +1119,7 @@ def meshcode_status() -> Dict[str, Any]:
|
|
|
1151
1119
|
@mcp.tool()
|
|
1152
1120
|
@with_working_status
|
|
1153
1121
|
def meshcode_register(role: str = "") -> Dict[str, Any]:
|
|
1154
|
-
"""Re-register
|
|
1155
|
-
want to update your role description.
|
|
1156
|
-
"""
|
|
1122
|
+
"""Re-register agent (update role)."""
|
|
1157
1123
|
return be.register_agent(PROJECT_NAME, AGENT_NAME, role or AGENT_ROLE)
|
|
1158
1124
|
|
|
1159
1125
|
|
|
@@ -1171,10 +1137,7 @@ def meshcode_set_status(status: str, task: str = "") -> Dict[str, Any]:
|
|
|
1171
1137
|
@mcp.tool()
|
|
1172
1138
|
@with_working_status
|
|
1173
1139
|
def meshcode_init(project: str, agent: str, role: str = "") -> Dict[str, Any]:
|
|
1174
|
-
"""
|
|
1175
|
-
if you need to dynamically switch context — normally the env vars set at
|
|
1176
|
-
server start are correct.
|
|
1177
|
-
"""
|
|
1140
|
+
"""Switch project/agent context. Rarely needed."""
|
|
1178
1141
|
global PROJECT_NAME, AGENT_NAME, AGENT_ROLE, _PROJECT_ID
|
|
1179
1142
|
PROJECT_NAME = project
|
|
1180
1143
|
AGENT_NAME = agent
|
|
@@ -1191,18 +1154,7 @@ def meshcode_init(project: str, agent: str, role: str = "") -> Dict[str, Any]:
|
|
|
1191
1154
|
@with_working_status
|
|
1192
1155
|
def meshcode_task_create(title: str, description: str = "", assignee: str = "*",
|
|
1193
1156
|
priority: str = "normal", parent_task_id: Optional[str] = None) -> Dict[str, Any]:
|
|
1194
|
-
"""Create
|
|
1195
|
-
the commander) want to assign work that needs tracking.
|
|
1196
|
-
|
|
1197
|
-
Args:
|
|
1198
|
-
title: Short title of the task.
|
|
1199
|
-
description: Longer description / acceptance criteria.
|
|
1200
|
-
assignee: Agent name to assign to, or "*" for any agent (anyone can claim).
|
|
1201
|
-
priority: 'low' / 'normal' / 'high' / 'urgent'.
|
|
1202
|
-
parent_task_id: Optional parent task id for nesting subtasks.
|
|
1203
|
-
|
|
1204
|
-
Returns: {"ok": true, "task_id": "...", "title": "..."}
|
|
1205
|
-
"""
|
|
1157
|
+
"""Create task. assignee="*" for any, priority: low/normal/high/urgent."""
|
|
1206
1158
|
api_key = _get_api_key()
|
|
1207
1159
|
result = be.task_create(api_key, _PROJECT_ID, AGENT_NAME, title,
|
|
1208
1160
|
description=description, assignee=assignee,
|
|
@@ -1239,13 +1191,7 @@ def meshcode_tasks(status_filter: Optional[str] = None) -> Dict[str, Any]:
|
|
|
1239
1191
|
@mcp.tool()
|
|
1240
1192
|
@with_working_status
|
|
1241
1193
|
def meshcode_task_claim(task_id: str) -> Dict[str, Any]:
|
|
1242
|
-
"""
|
|
1243
|
-
else (or assigned to a different agent), this returns an error and another
|
|
1244
|
-
task should be picked.
|
|
1245
|
-
|
|
1246
|
-
Args:
|
|
1247
|
-
task_id: The uuid of the task you want to claim.
|
|
1248
|
-
"""
|
|
1194
|
+
"""Claim an open task. Fails if already claimed by someone else."""
|
|
1249
1195
|
api_key = _get_api_key()
|
|
1250
1196
|
return be.task_claim(api_key, _PROJECT_ID, task_id, AGENT_NAME)
|
|
1251
1197
|
|
|
@@ -1253,12 +1199,7 @@ def meshcode_task_claim(task_id: str) -> Dict[str, Any]:
|
|
|
1253
1199
|
@mcp.tool()
|
|
1254
1200
|
@with_working_status
|
|
1255
1201
|
def meshcode_task_complete(task_id: str, summary: str = "") -> Dict[str, Any]:
|
|
1256
|
-
"""
|
|
1257
|
-
|
|
1258
|
-
Args:
|
|
1259
|
-
task_id: The uuid of the task you previously claimed.
|
|
1260
|
-
summary: Short description of what was accomplished + any artifacts.
|
|
1261
|
-
"""
|
|
1202
|
+
"""Complete a claimed task with summary."""
|
|
1262
1203
|
api_key = _get_api_key()
|
|
1263
1204
|
return be.task_complete(api_key, _PROJECT_ID, task_id, AGENT_NAME, summary=summary)
|
|
1264
1205
|
|
|
@@ -1266,11 +1207,7 @@ def meshcode_task_complete(task_id: str, summary: str = "") -> Dict[str, Any]:
|
|
|
1266
1207
|
@mcp.tool()
|
|
1267
1208
|
@with_working_status
|
|
1268
1209
|
def meshcode_task_approve(task_id: str) -> Dict[str, Any]:
|
|
1269
|
-
"""Approve a task
|
|
1270
|
-
|
|
1271
|
-
Args:
|
|
1272
|
-
task_id: Task UUID to approve.
|
|
1273
|
-
"""
|
|
1210
|
+
"""Approve a reviewed task."""
|
|
1274
1211
|
api_key = _get_api_key()
|
|
1275
1212
|
return be.sb_rpc("mc_task_approve", {
|
|
1276
1213
|
"p_api_key": api_key,
|
|
@@ -1283,12 +1220,7 @@ def meshcode_task_approve(task_id: str) -> Dict[str, Any]:
|
|
|
1283
1220
|
@mcp.tool()
|
|
1284
1221
|
@with_working_status
|
|
1285
1222
|
def meshcode_task_reject(task_id: str, feedback: str = "") -> Dict[str, Any]:
|
|
1286
|
-
"""Reject a task
|
|
1287
|
-
|
|
1288
|
-
Args:
|
|
1289
|
-
task_id: Task UUID to reject.
|
|
1290
|
-
feedback: Why it was rejected.
|
|
1291
|
-
"""
|
|
1223
|
+
"""Reject a task, sends back with feedback."""
|
|
1292
1224
|
api_key = _get_api_key()
|
|
1293
1225
|
return be.sb_rpc("mc_task_reject", {
|
|
1294
1226
|
"p_api_key": api_key,
|
|
@@ -1305,13 +1237,7 @@ def meshcode_task_reject(task_id: str, feedback: str = "") -> Dict[str, Any]:
|
|
|
1305
1237
|
@with_working_status
|
|
1306
1238
|
def meshcode_link(target_meshwork: str, source_agents: Optional[str] = None,
|
|
1307
1239
|
target_agents: Optional[str] = None) -> Dict[str, Any]:
|
|
1308
|
-
"""
|
|
1309
|
-
|
|
1310
|
-
Args:
|
|
1311
|
-
target_meshwork: Meshwork name to link to.
|
|
1312
|
-
source_agents: Comma-separated allowed senders (default: all).
|
|
1313
|
-
target_agents: Comma-separated allowed receivers (default: all).
|
|
1314
|
-
"""
|
|
1240
|
+
"""Link to another meshwork. Target must accept."""
|
|
1315
1241
|
api_key = _get_api_key()
|
|
1316
1242
|
src = source_agents.split(",") if source_agents else ["*"]
|
|
1317
1243
|
tgt = target_agents.split(",") if target_agents else ["*"]
|
|
@@ -1353,12 +1279,7 @@ def meshcode_links() -> Dict[str, Any]:
|
|
|
1353
1279
|
@mcp.tool()
|
|
1354
1280
|
@with_working_status
|
|
1355
1281
|
def meshcode_expand_link(link_id: str, agents: str) -> Dict[str, Any]:
|
|
1356
|
-
"""
|
|
1357
|
-
|
|
1358
|
-
Args:
|
|
1359
|
-
link_id: UUID of the active link to expand.
|
|
1360
|
-
agents: Comma-separated agent names to add (applied to both sides).
|
|
1361
|
-
"""
|
|
1282
|
+
"""Add agents to an active mesh link."""
|
|
1362
1283
|
api_key = _get_api_key()
|
|
1363
1284
|
agent_list = [a.strip() for a in agents.split(",")]
|
|
1364
1285
|
return be.sb_rpc("mc_expand_mesh_link", {
|
|
@@ -1374,11 +1295,7 @@ def meshcode_expand_link(link_id: str, agents: str) -> Dict[str, Any]:
|
|
|
1374
1295
|
@mcp.tool()
|
|
1375
1296
|
@with_working_status
|
|
1376
1297
|
def meshcode_create_meshwork(name: str) -> Dict[str, Any]:
|
|
1377
|
-
"""Create a new meshwork
|
|
1378
|
-
|
|
1379
|
-
Args:
|
|
1380
|
-
name: Meshwork name (lowercase, hyphens ok).
|
|
1381
|
-
"""
|
|
1298
|
+
"""Create a new meshwork."""
|
|
1382
1299
|
api_key = _get_api_key()
|
|
1383
1300
|
result = be.sb_rpc("mc_create_meshwork_by_api_key", {
|
|
1384
1301
|
"p_api_key": api_key, "p_name": name,
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Static-analysis test: every name used inside with_working_status must exist.
|
|
2
|
+
|
|
3
|
+
This is the test that catches the _PROCESSING_COOLDOWN_S class of bugs —
|
|
4
|
+
references to variables that were never defined. It parses server.py's AST
|
|
5
|
+
and checks that every Name node inside the wrapper functions resolves to a
|
|
6
|
+
local, global, or builtin in the module.
|
|
7
|
+
|
|
8
|
+
Run: python -m pytest meshcode/meshcode_mcp/test_server_wrapper.py -v
|
|
9
|
+
or: python -m unittest meshcode.meshcode_mcp.test_server_wrapper
|
|
10
|
+
"""
|
|
11
|
+
import ast
|
|
12
|
+
import builtins
|
|
13
|
+
import os
|
|
14
|
+
import unittest
|
|
15
|
+
|
|
16
|
+
_SERVER_PATH = os.path.join(os.path.dirname(__file__), "server.py")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _get_module_globals(tree: ast.Module) -> set[str]:
|
|
20
|
+
"""Collect all names assigned/imported at module level."""
|
|
21
|
+
names = set()
|
|
22
|
+
for node in ast.walk(tree):
|
|
23
|
+
if isinstance(node, ast.Import):
|
|
24
|
+
for alias in node.names:
|
|
25
|
+
names.add(alias.asname or alias.name.split(".")[0])
|
|
26
|
+
elif isinstance(node, ast.ImportFrom):
|
|
27
|
+
for alias in node.names:
|
|
28
|
+
names.add(alias.asname or alias.name)
|
|
29
|
+
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
30
|
+
names.add(node.name)
|
|
31
|
+
elif isinstance(node, ast.ClassDef):
|
|
32
|
+
names.add(node.name)
|
|
33
|
+
elif isinstance(node, ast.Assign):
|
|
34
|
+
for target in node.targets:
|
|
35
|
+
if isinstance(target, ast.Name):
|
|
36
|
+
names.add(target.id)
|
|
37
|
+
elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
|
|
38
|
+
names.add(node.target.id)
|
|
39
|
+
elif isinstance(node, ast.AugAssign) and isinstance(node.target, ast.Name):
|
|
40
|
+
names.add(node.target.id)
|
|
41
|
+
# builtins
|
|
42
|
+
names.update(dir(builtins))
|
|
43
|
+
return names
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _collect_names_in_func(func_node: ast.AST) -> set[str]:
|
|
47
|
+
"""All Name.id references inside a function body."""
|
|
48
|
+
refs = set()
|
|
49
|
+
for node in ast.walk(func_node):
|
|
50
|
+
if isinstance(node, ast.Name):
|
|
51
|
+
refs.add(node.id)
|
|
52
|
+
return refs
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _collect_locals(func_node: ast.AST) -> set[str]:
|
|
56
|
+
"""Names assigned, declared global, or used as parameters inside a function."""
|
|
57
|
+
local = set()
|
|
58
|
+
for node in ast.walk(func_node):
|
|
59
|
+
if isinstance(node, ast.Global):
|
|
60
|
+
local.update(node.names)
|
|
61
|
+
elif isinstance(node, ast.Assign):
|
|
62
|
+
for t in node.targets:
|
|
63
|
+
if isinstance(t, ast.Name):
|
|
64
|
+
local.add(t.id)
|
|
65
|
+
elif isinstance(node, ast.arg):
|
|
66
|
+
local.add(node.arg)
|
|
67
|
+
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
68
|
+
local.add(node.name)
|
|
69
|
+
for arg in node.args.args:
|
|
70
|
+
local.add(arg.arg)
|
|
71
|
+
if node.args.vararg:
|
|
72
|
+
local.add(node.args.vararg.arg)
|
|
73
|
+
if node.args.kwarg:
|
|
74
|
+
local.add(node.args.kwarg.arg)
|
|
75
|
+
return local
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ServerWrapperNamesTest(unittest.TestCase):
|
|
79
|
+
"""Verify no undefined names inside with_working_status."""
|
|
80
|
+
|
|
81
|
+
def test_no_undefined_names_in_wrapper(self):
|
|
82
|
+
with open(_SERVER_PATH) as f:
|
|
83
|
+
tree = ast.parse(f.read(), _SERVER_PATH)
|
|
84
|
+
|
|
85
|
+
module_globals = _get_module_globals(tree)
|
|
86
|
+
|
|
87
|
+
# Find the with_working_status function
|
|
88
|
+
wrapper_func = None
|
|
89
|
+
for node in ast.walk(tree):
|
|
90
|
+
if isinstance(node, ast.FunctionDef) and node.name == "with_working_status":
|
|
91
|
+
wrapper_func = node
|
|
92
|
+
break
|
|
93
|
+
|
|
94
|
+
self.assertIsNotNone(wrapper_func, "with_working_status not found in server.py")
|
|
95
|
+
|
|
96
|
+
# Check every nested function inside with_working_status
|
|
97
|
+
for node in ast.walk(wrapper_func):
|
|
98
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
99
|
+
if node.name == "with_working_status":
|
|
100
|
+
continue # skip the outer function itself
|
|
101
|
+
refs = _collect_names_in_func(node)
|
|
102
|
+
locals_ = _collect_locals(node)
|
|
103
|
+
# Add the outer function's params and locals too
|
|
104
|
+
outer_locals = _collect_locals(wrapper_func)
|
|
105
|
+
allowed = module_globals | locals_ | outer_locals
|
|
106
|
+
|
|
107
|
+
undefined = refs - allowed
|
|
108
|
+
self.assertEqual(
|
|
109
|
+
undefined,
|
|
110
|
+
set(),
|
|
111
|
+
f"Undefined names in {node.name}() inside with_working_status: "
|
|
112
|
+
f"{undefined}. These will cause NameError at runtime.",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
if __name__ == "__main__":
|
|
117
|
+
unittest.main()
|
|
@@ -24,4 +24,5 @@ meshcode/meshcode_mcp/backend.py
|
|
|
24
24
|
meshcode/meshcode_mcp/realtime.py
|
|
25
25
|
meshcode/meshcode_mcp/server.py
|
|
26
26
|
meshcode/meshcode_mcp/test_backend.py
|
|
27
|
-
meshcode/meshcode_mcp/test_realtime.py
|
|
27
|
+
meshcode/meshcode_mcp/test_realtime.py
|
|
28
|
+
meshcode/meshcode_mcp/test_server_wrapper.py
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|