"""TinqsMDBridge -- Marvelous Designer python plug-in (v2, main-loop design). Runs a localhost-only TCP socket server inside Marvelous Designer and executes Python source sent to it, giving external tooling (Claude Code) live control of MD's scripting API (pattern_api / import_api / export_api / fabric_api / utility_api / ApiTypes). WHY v2 BLOCKS THE UI -- verified 2026-07-30 on MD 2026 Personal (embedded Python 3.11.8): MD's embedded interpreter does NOT schedule Python background threads while MD is idle. A daemon-thread server binds and listens (the OS accepts connections into the backlog) but accept()/recv() never run, so every request times out. No Python Qt binding ships with MD either, so the iClone QTimer trick is unavailable. The only thread Python code reliably runs on is the one the Plug-in Manager calls into -- so the server IS the click: - Clicking Plugin > TinqsMDBridge enters a single-threaded serve loop on the main thread. MD's UI freezes ("Not Responding" is NORMAL) while the bridge session is active. All api calls run on the main thread -- the safest place for them. - The loop exits (returning MD to interactive use) when a client sends the literal code "__STOP__" (python tools/md_bridge.py --stop), or after IDLE_TIMEOUT_S with no requests (default 300, env TINQS_MD_BRIDGE_IDLE overrides) -- the escape hatch, since a frozen UI can't be clicked. Each click is a fresh session; the socket is fully closed on exit. Install (one-time, manual -- MD has no auto-load plugin folder): 1. In MD: Plugin tab > Plug-in Manager > + ADD 2. Select this file, name it "TinqsMDBridge", click OK 3. Click Plugin > TinqsMDBridge to START A BRIDGE SESSION (UI freezes) 4. Verify from a terminal: python tools/md_bridge.py --ping 5. End the session: python tools/md_bridge.py --stop NOTE: if MD copies the .py on registration rather than referencing it in place (check --ping's "source" field), edits require remove + re-ADD. Wire protocol (identical to the iClone TinqsBridge): newline-delimited JSON over TCP, one request/response pair per connection. request: {"id": , "code": ""} success: {"id": , "ok": true, "result": , "stdout": ""} failure: {"id": , "ok": false, "error": "", "stdout": ""} Execution semantics (same `result` convention as the iClone bridge): - exec(code, ns, ns) against ONE persistent namespace. It survives across requests AND across serve sessions (module-level dict; the Plug-in Manager re-execs this file per click but the namespace is re-seeded, fresh api module refs, stale user state discarded -- keep long-lived state on your own side). - Setting `result` in submitted code makes it the response result (JSON-encoded, repr() fallback). Cleared before every exec. - stdout/stderr captured and returned; exceptions come back as ok:false with a traceback and the session keeps serving. Log file: %TEMP%/tinqs_md_bridge.log """ import contextlib import io import json import os import socket import time import traceback from datetime import datetime VERSION = 2 DEFAULT_PORT = 18900 HOST = "127.0.0.1" ACCEPT_POLL_TIMEOUT_S = 1.0 CONN_READ_TIMEOUT_S = 30.0 DEFAULT_IDLE_TIMEOUT_S = 14400.0 # 4 h -- one click lasts a work session; # --stop from any terminal ends it anytime STOP_SENTINEL = "__STOP__" LOG_PATH = os.path.join( os.environ.get("TEMP", os.environ.get("TMP", ".")), "tinqs_md_bridge.log", ) def _log(msg): """Append a timestamped line to the log file. Must never raise.""" try: line = "[{}] {}\n".format(datetime.now().strftime("%Y-%m-%d %H:%M:%S"), msg) with open(LOG_PATH, "a", encoding="utf-8") as f: f.write(line) except Exception: pass def _env_float(name, default): raw = os.environ.get(name) if raw: try: return float(raw) except ValueError: _log("WARN: {}={!r} not a number, using {}".format(name, raw, default)) return default def _import_md_api(): """Import whatever MD api modules exist in this MD build; report misses.""" ns = {} missing = [] for name in ("import_api", "export_api", "pattern_api", "fabric_api", "utility_api", "ApiTypes"): try: ns[name] = __import__(name) except Exception: missing.append(name) return ns, missing def _execute(ns, code): ns["result"] = None out = io.StringIO() try: with contextlib.redirect_stdout(out), contextlib.redirect_stderr(out): exec(code, ns, ns) result = ns.get("result") try: json.dumps(result) except TypeError: result = repr(result) return {"ok": True, "result": result, "stdout": out.getvalue()} except Exception: return {"ok": False, "error": traceback.format_exc(), "stdout": out.getvalue()} def _read_request(conn): """Read one newline-terminated JSON request. Returns dict or None.""" conn.settimeout(CONN_READ_TIMEOUT_S) buf = b"" while not buf.endswith(b"\n"): chunk = conn.recv(4096) if not chunk: break buf += chunk if not buf.strip(): return None return json.loads(buf.decode("utf-8")) def _respond(conn, resp): conn.sendall((json.dumps(resp) + "\n").encode("utf-8")) def _reclaim_stale_socket(): """v1 of this plugin parked a never-serving socket on builtins. Close it so our bind succeeds without an MD restart.""" import builtins old = getattr(builtins, "_tinqs_md_bridge_singleton", None) if old is not None: try: if getattr(old, "server_socket", None) is not None: old.server_socket.close() _log("closed stale v1 socket") except Exception: pass setattr(builtins, "_tinqs_md_bridge_singleton", None) def _serve(): import sys port = int(_env_float("TINQS_MD_BRIDGE_PORT", DEFAULT_PORT)) idle_timeout = _env_float("TINQS_MD_BRIDGE_IDLE", DEFAULT_IDLE_TIMEOUT_S) _log("=" * 60) _log("TinqsMDBridge v{} session starting, port {}, idle timeout {}s".format( VERSION, port, idle_timeout)) _reclaim_stale_socket() sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: sock.bind((HOST, port)) except OSError: _log("BIND FAILED on {}:{}:\n{}".format(HOST, port, traceback.format_exc())) print("TinqsMDBridge: port {} busy -- another session active? see {}".format( port, LOG_PATH)) return sock.listen(5) sock.settimeout(ACCEPT_POLL_TIMEOUT_S) api_ns, missing = _import_md_api() try: source = os.path.abspath(__file__) except NameError: source = "" ns = dict(api_ns) ns["BRIDGE"] = { "version": VERSION, "mode": "mainloop", "port": port, "idle_timeout_s": idle_timeout, "source": source, "api_modules": sorted(api_ns.keys()), "api_missing": missing, "python": sys.version, } _log("serving on {}:{} api={} missing={} python={}".format( HOST, port, sorted(api_ns.keys()), missing, sys.version.split()[0])) print("TinqsMDBridge v{}: serving on {}:{}".format(VERSION, HOST, port)) print("MD's UI is frozen while the bridge session runs -- this is normal.") print("End the session with: python tools/md_bridge.py --stop") print("(auto-ends after {:.0f}s idle)".format(idle_timeout)) served = 0 deadline = time.monotonic() + idle_timeout try: while True: if time.monotonic() > deadline: _log("idle timeout after {} requests -- session over".format(served)) print("TinqsMDBridge: idle timeout, session over (served {})".format(served)) return try: conn, _addr = sock.accept() except socket.timeout: continue except OSError: _log("server socket died:\n{}".format(traceback.format_exc())) return try: try: req = _read_request(conn) except Exception: _respond(conn, {"id": None, "ok": False, "error": "invalid JSON request", "stdout": ""}) continue if req is None: continue code = req.get("code", "") if code.strip() == STOP_SENTINEL: _respond(conn, {"id": req.get("id"), "ok": True, "result": "stopped", "stdout": ""}) _log("stop command -- session over, served {}".format(served)) print("TinqsMDBridge: stopped by client (served {})".format(served)) return resp = _execute(ns, code) resp["id"] = req.get("id") served += 1 deadline = time.monotonic() + idle_timeout if not resp["ok"]: last = resp["error"].strip().splitlines()[-1] if resp["error"] else "?" _log("request id={} ok=false: {}".format(req.get("id"), last)) _respond(conn, resp) except Exception: _log("connection error:\n{}".format(traceback.format_exc())) finally: try: conn.close() except Exception: pass finally: try: sock.close() except Exception: pass _log("socket closed, session ended") try: _serve() except Exception: _log("session CRASHED:\n{}".format(traceback.format_exc())) print("TinqsMDBridge: crashed -- see {}".format(LOG_PATH))