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.
Files changed (30) hide show
  1. {meshcode-2.1.1 → meshcode-2.2.0}/PKG-INFO +1 -1
  2. {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/__init__.py +1 -1
  3. {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/meshcode_mcp/server.py +62 -145
  4. meshcode-2.2.0/meshcode/meshcode_mcp/test_server_wrapper.py +117 -0
  5. {meshcode-2.1.1 → meshcode-2.2.0}/meshcode.egg-info/PKG-INFO +1 -1
  6. {meshcode-2.1.1 → meshcode-2.2.0}/meshcode.egg-info/SOURCES.txt +2 -1
  7. {meshcode-2.1.1 → meshcode-2.2.0}/pyproject.toml +1 -1
  8. {meshcode-2.1.1 → meshcode-2.2.0}/README.md +0 -0
  9. {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/cli.py +0 -0
  10. {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/comms_v4.py +0 -0
  11. {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/invites.py +0 -0
  12. {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/launcher.py +0 -0
  13. {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/launcher_install.py +0 -0
  14. {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/meshcode_mcp/__init__.py +0 -0
  15. {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/meshcode_mcp/__main__.py +0 -0
  16. {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/meshcode_mcp/backend.py +0 -0
  17. {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/meshcode_mcp/realtime.py +0 -0
  18. {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/meshcode_mcp/test_backend.py +0 -0
  19. {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  20. {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/preferences.py +0 -0
  21. {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/protocol_v2.py +0 -0
  22. {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/run_agent.py +0 -0
  23. {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/secrets.py +0 -0
  24. {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/self_update.py +0 -0
  25. {meshcode-2.1.1 → meshcode-2.2.0}/meshcode/setup_clients.py +0 -0
  26. {meshcode-2.1.1 → meshcode-2.2.0}/meshcode.egg-info/dependency_links.txt +0 -0
  27. {meshcode-2.1.1 → meshcode-2.2.0}/meshcode.egg-info/entry_points.txt +0 -0
  28. {meshcode-2.1.1 → meshcode-2.2.0}/meshcode.egg-info/requires.txt +0 -0
  29. {meshcode-2.1.1 → meshcode-2.2.0}/meshcode.egg-info/top_level.txt +0 -0
  30. {meshcode-2.1.1 → meshcode-2.2.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.1.1
3
+ Version: 2.2.0
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -1,2 +1,2 @@
1
1
  """MeshCode — Real-time communication between AI agents."""
2
- __version__ = "2.1.1"
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, _processing_timer
435
+ global _last_tool_at
426
436
  _last_tool_at = _time.time()
427
- _set_state("processing", "processing...")
428
- _processing_timer = _threading.Timer(_PROCESSING_COOLDOWN_S, _processing_to_online)
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
- BEHAVIOR LOOP (your default state never exit unless told):
569
- 1. Act on task/message 2. meshcode_send if needed 3. meshcode_wait()
570
- 4. On message back to 1. On timeout meshcode_wait(2x timeout, cap 1800s).
571
- Reset to 60s on any message. On got_done exit loop, summarize, stop.
572
- 5. Only break loop if: user says stop, fatal error, or "tell the human X".
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
- - NEVER use CLI commands (meshcode watch, meshcode read, meshcode send) in bash.
576
- Use ONLY the MCP tools: meshcode_check, meshcode_read, meshcode_send, meshcode_wait.
577
- CLI commands are for humans in terminal. You have MCP tools — use them.
578
- - Tasks > messages. Use meshcode_tasks/claim/complete for trackable work.
579
- - Messages <100 tokens, signal-only. Long content → create task instead.
580
- - No empty acks ("OK"/"Got it"). No prose padding. JSON reports only.
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: check parent process (editor/LLM) CPU usage
720
- if _current_state not in ("waiting",): # Don't override explicit WAITING
721
- parent_cpu = _get_parent_cpu()
722
- if parent_cpu > 5.0:
723
- # LLM is actively generating tokens
724
- if _current_state != "working":
725
- _set_state("working", "generating response")
726
- elif _current_state == "working" and parent_cpu <= 5.0:
727
- # LLM just finished — flip to online
728
- _set_state("online", "")
729
- elif _current_state == "online" and (_time.time() - _last_tool_at) > _IDLE_THRESHOLD_S:
730
- _set_state("idle", f"idle ({int((_time.time() - _last_tool_at) / 60)}m)")
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
- _heartbeat_stop.wait(30) # sleep but interruptible on shutdown
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 a message to an agent. Use "agent@meshwork" for cross-mesh.
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 mark-read all pending messages. ACKs senders.
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 recent message history (both read and unread). Use when you need
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 ID. Use when you need to re-read a message
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
- # On timeout, check for unclaimed tasks assigned to this agent
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
- """Non-blocking: returns pending message count + any new messages.
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 this agent. Use if disconnected or
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
- """Re-initialize MCP session for different project/agent. Use ONLY
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 a task in the shared backlog. Usually called by
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
- """Atomically claim an open task. Fails if already claimed by someone
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
- """Mark a task as done. Only the claimer can complete it.
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 in review. Only the reviewer can approve.
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 in review — sends it back to in_progress with feedback.
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
- """Create a link to another meshwork. Target owner must accept.
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
- """Expand an active mesh link to allow more agents through.
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 for the current user. Tell them to run 'meshcode setup <name> <agent>' next.
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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.1.1
3
+ Version: 2.2.0
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "meshcode"
7
- version = "2.1.1"
7
+ version = "2.2.0"
8
8
  description = "Real-time communication between AI agents — Supabase-backed CLI"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes