fix: control port needs a per-launch token; MCP tool annotations tell the whole truth (#205) (#901)

solidcraft --control PORT writes a random per-launch token to a user-only file (<data>/control/PORT.token; 0700/0600 on Unix, icacls on Windows; SOLIDCRAFT_CONTROL_TOKEN overrides) and refuses any connection whose first request lacks it. mcp --connect, the demo and packaging smoke clients send it. MCP execute/batch/open/save/export/screenshot are annotated destructive and open-world, new_design destructive. Docs: control-protocol.md, mcp.md, AGENTS.md, pending release note in releasing.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bherbruckandClaude Opus 5.5 authored and GitHub committed 2026-10-13 18:51:19 -04:00
1 parent d9237d004e
commit fe3717f2e4
19 files changed
+465 -53

No files matched your search

+20 -1
View File
@@ -19,6 +19,7 @@ import socket
import subprocess
import sys
import time
from pathlib import Path
HERE = os.path.dirname(os.path.abspath(__file__))
@@ -63,10 +64,28 @@ class Mcp:
self.p.wait(timeout=10)
def control_token(port):
"""The app's control token: SOLIDCRAFT_CONTROL_TOKEN, else the token file the app wrote for
this port (docs/control-protocol.md)."""
if os.environ.get("SOLIDCRAFT_CONTROL_TOKEN"):
return os.environ["SOLIDCRAFT_CONTROL_TOKEN"].strip()
if os.environ.get("SOLIDCRAFT_CONTROL_DIR"):
folder = Path(os.environ["SOLIDCRAFT_CONTROL_DIR"])
elif sys.platform == "win32":
folder = Path(os.environ["LOCALAPPDATA"]) / "SolidCraft" / "control"
elif sys.platform == "darwin":
folder = Path.home() / "Library/Application Support/SolidCraft/control"
else:
data = os.environ.get("XDG_DATA_HOME") or str(Path.home() / ".local/share")
folder = Path(data) / "solidcraft" / "control"
return (folder / f"{port}.token").read_text().strip()
def control(port, method, params=None):
"""One request on the app's control channel (camera moves)."""
with socket.create_connection(("127.0.0.1", port)) as s:
s.sendall((json.dumps({"id": 1, "method": method, "params": params or {}}) + "\n").encode())
msg = {"id": 1, "method": method, "params": params or {}, "token": control_token(port)}
s.sendall((json.dumps(msg) + "\n").encode())
buf = b""
while not buf.endswith(b"\n"):
chunk = s.recv(1 << 16)