From afbf826b0cf5bca44acf61a05be278cd673d7d39 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 4 Sep 2026 23:58:39 +0300 Subject: [PATCH] Ship client-neutral MCP bundles --- .github/workflows/control.yml | 5 +- .github/workflows/release.yml | 112 +++++++++++++ README.md | 2 +- docs/automation/README.md | 47 ++---- docs/automation/connect.sh | 65 -------- docs/automation/install.sh | 19 --- docs/automation/live.py | 130 --------------- docs/automation/ocs.py | 153 ------------------ docs/readme/README.ar.md | 3 +- docs/readme/README.bg.md | 3 +- docs/readme/README.cs.md | 3 +- docs/readme/README.de.md | 3 +- docs/readme/README.es.md | 3 +- docs/readme/README.fi.md | 3 +- docs/readme/README.fr.md | 3 +- docs/readme/README.hi.md | 3 +- docs/readme/README.hu.md | 3 +- docs/readme/README.it.md | 3 +- docs/readme/README.ja.md | 3 +- docs/readme/README.ko.md | 3 +- docs/readme/README.nl.md | 3 +- docs/readme/README.pl.md | 3 +- docs/readme/README.pt-BR.md | 3 +- docs/readme/README.ru.md | 3 +- docs/readme/README.tr.md | 3 +- docs/readme/README.zh-CN.md | 3 +- docs/readme/README.zh-TW.md | 3 +- .../codex/opencadstudio-control/SKILL.md | 20 --- .../opencadstudio-control/agents/openai.yaml | 4 - packaging/mcpb/pack.py | 117 ++++++++++++++ 30 files changed, 285 insertions(+), 446 deletions(-) delete mode 100755 docs/automation/connect.sh delete mode 100755 docs/automation/install.sh delete mode 100644 docs/automation/live.py delete mode 100644 docs/automation/ocs.py delete mode 100644 integrations/codex/opencadstudio-control/SKILL.md delete mode 100644 integrations/codex/opencadstudio-control/agents/openai.yaml create mode 100644 packaging/mcpb/pack.py diff --git a/.github/workflows/control.yml b/.github/workflows/control.yml index a3196a561..83b2bcd2e 100644 --- a/.github/workflows/control.yml +++ b/.github/workflows/control.yml @@ -17,9 +17,9 @@ jobs: - uses: dtolnay/rust-toolchain@stable - name: Validate control contract run: cargo test --lib app::control::tests - - name: Validate Python adapters + - name: Validate MCP packaging scripts if: runner.os != 'Windows' - run: python3 -m py_compile docs/automation/live.py docs/automation/mcp_smoke.py + run: python3 -m py_compile docs/automation/mcp_smoke.py packaging/mcpb/pack.py mcp: runs-on: ubuntu-latest steps: @@ -28,6 +28,7 @@ jobs: - run: cargo test --lib mcp::tests - run: cargo build - run: python3 docs/automation/mcp_smoke.py target/debug/OpenCADStudio + - run: python3 packaging/mcpb/pack.py --version 0.0.0 --linux /bin/true --windows /bin/true --macos /bin/true --output /tmp/OpenCADStudio.mcpb --server-json /tmp/server.json web: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 88a7dcbfa..de159d5dc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -233,6 +233,14 @@ jobs: timestamp-rfc3161: http://timestamp.acs.microsoft.com timestamp-digest: SHA256 + - name: Upload signed executable for MCP Bundle + uses: actions/upload-artifact@v4 + with: + name: mcpb-windows-binary + path: target/release/OpenCADStudio.exe + if-no-files-found: error + retention-days: 7 + - name: Build MSI installer shell: pwsh run: | @@ -428,6 +436,14 @@ jobs: codesign --force --deep --sign - --timestamp=none OpenCADStudio.app codesign --verify --strict --verbose=2 OpenCADStudio.app + - name: Upload signed executable for MCP Bundle + uses: actions/upload-artifact@v4 + with: + name: mcpb-macos-binary + path: OpenCADStudio.app/Contents/MacOS/OpenCADStudio + if-no-files-found: error + retention-days: 7 + - name: Create .dmg run: | # hdiutil intermittently fails with "Resource busy" on the hosted @@ -475,3 +491,99 @@ jobs: echo " sha256 \"$SHA\"" echo '```' } >> "$GITHUB_STEP_SUMMARY" + + build-mcpb: + if: github.event_name == 'release' || inputs.target == 'all' + needs: [build-appimage, build-windows, build-macos] + runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Download Linux executable + uses: actions/download-artifact@v4 + with: + name: linux-payload + path: mcpb/linux + + - name: Download Windows executable + uses: actions/download-artifact@v4 + with: + name: mcpb-windows-binary + path: mcpb/windows + + - name: Download macOS executable + uses: actions/download-artifact@v4 + with: + name: mcpb-macos-binary + path: mcpb/macos + + - name: Set package version + id: package + env: + EVENT_NAME: ${{ github.event_name }} + REF_NAME: ${{ github.ref_name }} + run: | + if [ "$EVENT_NAME" = "release" ]; then + VERSION="${REF_NAME#v}" + if [ "v$VERSION" != "$REF_NAME" ]; then + echo "::error::Release tag must be vX.Y.Z" + exit 1 + fi + else + VERSION="$(sed -n 's/^version = "\([0-9][0-9.]*\)"/\1/p' Cargo.toml | head -1)" + fi + if ! printf '%s' "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "::error::Invalid package version: $VERSION" + exit 1 + fi + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Build MCP Bundle + env: + VERSION: ${{ steps.package.outputs.version }} + run: | + tar -C mcpb/linux -xzf mcpb/linux/linux-payload.tar.gz + chmod +x \ + mcpb/linux/usr/bin/OpenCADStudio \ + mcpb/windows/OpenCADStudio.exe \ + mcpb/macos/OpenCADStudio + python3 packaging/mcpb/pack.py \ + --version "$VERSION" \ + --linux mcpb/linux/usr/bin/OpenCADStudio \ + --windows mcpb/windows/OpenCADStudio.exe \ + --macos mcpb/macos/OpenCADStudio \ + --output "OpenCADStudio-v${VERSION}.mcpb" \ + --server-json server.json + + - name: Upload MCP Bundle to release + if: github.event_name == 'release' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ steps.package.outputs.version }} + run: gh release upload "${{ github.ref_name }}" "OpenCADStudio-v${VERSION}.mcpb" server.json --clobber --repo "${{ github.repository }}" + + - name: Install MCP Registry publisher + if: github.event_name == 'release' + run: | + curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher + + - name: Publish to official MCP Registry + if: github.event_name == 'release' + run: | + ./mcp-publisher login github-oidc + ./mcp-publisher publish server.json + + - name: Upload MCP Bundle as workflow artifact + if: github.event_name == 'workflow_dispatch' + uses: actions/upload-artifact@v4 + with: + name: OpenCADStudio-v${{ steps.package.outputs.version }}-mcpb + path: | + OpenCADStudio-v${{ steps.package.outputs.version }}.mcpb + server.json + if-no-files-found: error diff --git a/README.md b/README.md index bac129f30..698b522d4 100644 --- a/README.md +++ b/README.md @@ -183,7 +183,7 @@ OpenCADStudio --serve --port 4242 OpenCADStudio --mcp ``` -The automation server exchanges one JSON object per line over standard input/output or a local TCP socket. The MCP endpoint exposes the live desktop editor through the same tools to every compatible client without an additional runtime. See the [automation guide](docs/automation/README.md) and the included [Python client](docs/automation/ocs.py). +The automation server exchanges one JSON object per line over standard input/output or a local TCP socket. The self-contained MCP endpoint exposes the live desktop editor through the same tools to every compatible client. Releases also publish a standard MCP Bundle and official registry metadata. See the [MCP control guide](docs/automation/README.md). ## Plugins diff --git a/docs/automation/README.md b/docs/automation/README.md index 6871b84d9..eb6029633 100644 --- a/docs/automation/README.md +++ b/docs/automation/README.md @@ -1,42 +1,23 @@ -# OpenCADStudio automation +# OpenCADStudio MCP control -`--serve` remains available for isolated headless scripts. `live.py` connects to the real desktop application through a private per-process descriptor under the user's OCS configuration directory. The GUI owns every mutation: transport threads only validate and enqueue requests. +Every native OpenCADStudio build contains the same MCP server as the editor. `OpenCADStudio --mcp` starts it over stdio, opens the desktop editor when needed, and exposes the live document without Python, a package manager, a sidecar service, or client-specific code. -The live protocol requires protocol version 1, stable session/document IDs, an expected edit revision for mutations, and a unique request ID. Commands report `waiting_input` while interactive work remains. Asynchronous file and renderer work remains `running` until its real callback finishes. A bounded operation cache makes retries with the same payload idempotent; after a restart or an expired operation, clients must read state instead of replaying a mutation. +The server provides four tools: -Operations include document state and activation, paged entity queries, layer/header/property inspection, command discovery, start/run/step/cancel, WCS/UCS/relative points, entity and structure picks, selection, property edits, semantic UI actions, undo/redo, protected save, event polling, and current-window PNG capture. The status bar includes a control switch whose green, yellow, and red states mean ready, busy, and off. +- `ocs_sessions` finds running editor sessions and opens OpenCADStudio when none exists. +- `ocs_read` reads document state, commands, entities, layers, properties, measurements, history, events, and operation status. +- `ocs_execute` performs semantic commands and input against the real editor. +- `ocs_capture` returns a PNG of the current window for visual verification. -The Web build exposes `ocs_control_submit(json)` and `ocs_control_take(ticket)`. They enter the same bounded GUI queue and return the same semantic responses; browser code polls the ticket until a response is present. +Mutations carry a session ID, document ID, expected revision, selection, and idempotent request ID. Commands report `waiting_input` while more input is required and asynchronous work remains `running` until its real callback finishes. Geometry stays in OpenCADStudio and its geometry kernel. -`OpenCADStudio --mcp` is the common local entry point for every MCP client. It is built into the application, uses stdio, launches the editor when needed, and has no Python or package-manager dependency. Tool definitions and safety instructions are served by MCP itself, so the drawing behavior is identical in every client. +Release builds also produce one standard MCP Bundle containing the native binaries for supported desktop platforms. The release workflow publishes its `server.json` metadata to the official MCP Registry. Compatible AI hosts can therefore discover and install the same server without an OpenCADStudio-specific adapter. -Run `./docs/automation/install.sh` once for a source build. It installs the release binary in `~/.local/share/ocs-control` and automatically connects installed Codex and Claude Code CLIs. Both registrations point to the same binary. Re-run an individual connection later with: +Registry discovery and permission prompts belong to the AI host. Hosts that implement MCP Registry and MCP Bundle support need no OpenCADStudio-specific setup; other hosts cannot be enabled silently by the application. + +The source-tree protocol smoke test uses only the Python standard library: ```sh -~/.local/share/ocs-control/connect.sh codex -~/.local/share/ocs-control/connect.sh claude-code -``` - -Any other local MCP client uses the same command and argument. `connect.sh config` prints the portable configuration object to paste into clients that do not provide a registration CLI: - -```json -{"mcpServers":{"opencadstudio":{"command":"/path/to/OpenCADStudio","args":["--mcp"]}}} -``` - -Client-specific skills and extension manifests may improve discovery, but contain no drawing or protocol implementation. Adding another AI client therefore requires only registering this command; the OpenCADStudio executable and tools do not change. - -The legacy client is unchanged: - -```python -from ocs import Ocs -with Ocs(binary="OpenCADStudio") as app: - app.new() - app.run("LINE 0,0 100,0") -``` - -For direct live diagnostics: - -```sh -python3 docs/automation/live.py --launch -python3 docs/automation/live.py '{"op":"state"}' +cargo build +python3 docs/automation/mcp_smoke.py target/debug/OpenCADStudio ``` diff --git a/docs/automation/connect.sh b/docs/automation/connect.sh deleted file mode 100755 index 5beae766f..000000000 --- a/docs/automation/connect.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/sh -set -eu - -ROOT=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) -SERVER=${OCS_MCP_COMMAND:-"$ROOT/OpenCADStudio"} -NAME=opencadstudio - -require_server() { - [ -x "$SERVER" ] || { - printf 'OpenCADStudio MCP executable not found: %s\n' "$SERVER" >&2 - exit 1 - } -} - -connect_codex() { - SKILLS=${CODEX_HOME:-"$HOME/.codex"}/skills - SOURCE="$ROOT/integrations/codex/opencadstudio-control" - if [ -f "$SOURCE/SKILL.md" ]; then - mkdir -p "$SKILLS/opencadstudio-control/agents" - cp "$SOURCE/SKILL.md" "$SKILLS/opencadstudio-control/SKILL.md" - cp "$SOURCE/agents/openai.yaml" "$SKILLS/opencadstudio-control/agents/openai.yaml" - fi - codex mcp remove "$NAME" >/dev/null 2>&1 || true - codex mcp add "$NAME" -- "$SERVER" --mcp - printf 'Connected Codex to OpenCADStudio.\n' -} - -connect_claude_code() { - claude mcp remove "$NAME" >/dev/null 2>&1 || true - claude mcp add --transport stdio --scope user "$NAME" -- "$SERVER" --mcp - printf 'Connected Claude Code to OpenCADStudio.\n' -} - -print_config() { - ESCAPED=$(printf '%s' "$SERVER" | sed 's/\\/\\\\/g; s/"/\\"/g') - printf '{"mcpServers":{"%s":{"command":"%s","args":["--mcp"]}}}\n' "$NAME" "$ESCAPED" -} - -require_server -case ${1:-all} in - all) - FOUND=false - if command -v codex >/dev/null 2>&1; then connect_codex; FOUND=true; fi - if command -v claude >/dev/null 2>&1; then connect_claude_code; FOUND=true; fi - if [ "$FOUND" = false ]; then - printf 'No supported AI CLI was found. Use this configuration in any MCP client:\n' - print_config - fi - ;; - codex) - command -v codex >/dev/null 2>&1 || { printf 'Codex CLI is not installed.\n' >&2; exit 1; } - connect_codex - ;; - claude-code|claude) - command -v claude >/dev/null 2>&1 || { printf 'Claude Code is not installed.\n' >&2; exit 1; } - connect_claude_code - ;; - config) - print_config - ;; - *) - printf 'Usage: %s [all|codex|claude-code|config]\n' "$0" >&2 - exit 2 - ;; -esac diff --git a/docs/automation/install.sh b/docs/automation/install.sh deleted file mode 100755 index 2a6c64c69..000000000 --- a/docs/automation/install.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh -set -eu -ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd) -DEST=${XDG_DATA_HOME:-"$HOME/.local/share"}/ocs-control -mkdir -p "$DEST" -cargo build --manifest-path "$ROOT/Cargo.toml" --release -cp "$ROOT/target/release/OpenCADStudio" "$DEST/OpenCADStudio.new" -chmod 755 "$DEST/OpenCADStudio.new" -mv -f "$DEST/OpenCADStudio.new" "$DEST/OpenCADStudio" -cp "$ROOT/docs/automation/live.py" "$DEST/live.py" -cp "$ROOT/docs/automation/connect.sh" "$DEST/connect.sh" -chmod 755 "$DEST/connect.sh" -mkdir -p "$DEST/integrations/codex/opencadstudio-control/agents" -cp "$ROOT/integrations/codex/opencadstudio-control/SKILL.md" "$DEST/integrations/codex/opencadstudio-control/SKILL.md" -cp "$ROOT/integrations/codex/opencadstudio-control/agents/openai.yaml" "$DEST/integrations/codex/opencadstudio-control/agents/openai.yaml" -rm -f "$DEST/mcp_server.py" -rm -rf "$DEST/venv" -"$DEST/connect.sh" all -printf 'Installed the client-neutral OpenCADStudio MCP runtime.\n' diff --git a/docs/automation/live.py b/docs/automation/live.py deleted file mode 100644 index eec6a22b4..000000000 --- a/docs/automation/live.py +++ /dev/null @@ -1,130 +0,0 @@ -"""Persistent local GUI client; no CAD implementation lives in this adapter.""" -from __future__ import annotations -import json -import os -from pathlib import Path -import socket -import subprocess -import sys -import time -import uuid -from typing import Any - -READ_OPS = {"state", "hello", "query", "entities", "layers", "header", "properties", "measure", "history", "commands", "events", "operation"} - - -def discovery_dir() -> Path: - if sys.platform == "win32": - base = Path(os.environ["APPDATA"]) - elif sys.platform == "darwin": - base = Path.home() / "Library/Application Support" - else: - base = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) - return base / "OpenCADStudio/automation" - - -def exchange(descriptor: dict, request: dict, timeout: float = 15) -> dict: - payload = {**request, "token": descriptor["token"], "session_id": descriptor["session_id"], "protocol": 1} - wire = (json.dumps(payload, ensure_ascii=False) + "\n").encode() - if len(wire) > 1_048_576: - raise ValueError("Request exceeds 1 MiB") - with socket.create_connection(("127.0.0.1", descriptor["port"]), timeout=timeout) as sock: - sock.sendall(wire) - with sock.makefile("rb") as stream: - line = stream.readline(16 * 1024 * 1024 + 1) - if not line or len(line) > 16 * 1024 * 1024: - raise ConnectionError("No valid OCS response; query request_id before retrying a mutation") - return json.loads(line) - - -def sessions() -> list[dict]: - found = [] - for path in sorted(discovery_dir().glob("*.json")): - descriptor = None - try: - if os.name == "posix" and (path.stat().st_uid != os.getuid() or path.stat().st_mode & 0o077): - continue - descriptor = json.loads(path.read_text()) - state = exchange(descriptor, {"op": "hello"}, timeout=1) - if state.get("ok") and state.get("session_id") == descriptor["session_id"]: - found.append({"descriptor": descriptor, "state": state}) - except (OSError, ValueError, KeyError): - # A crashed process cannot remove its descriptor. Only reap a file - # whose recorded PID is certainly gone; a busy live session keeps - # its discovery record for the next call. - try: - os.kill(int(descriptor["pid"]), 0) - except (ProcessLookupError, TypeError, KeyError): - try: - path.unlink() - except OSError: - pass - continue - return found - - -class LiveOcs: - def __init__(self, session_id: str | None = None): - available = sessions() - if session_id: - available = [s for s in available if s["state"]["session_id"] == session_id] - if len(available) != 1: - raise ValueError("Choose session_id from sessions; found " + str(len(available))) - self.descriptor = available[0]["descriptor"] - self.state = available[0]["state"] - self.client_id = uuid.uuid4().hex - - def request(self, request: dict, wait_seconds: float = 30) -> dict: - req = dict(request) - if req.get("op") not in READ_OPS: - req.setdefault("request_id", uuid.uuid4().hex) - req.setdefault("client_id", self.client_id) - req.setdefault("document_id", self.state["document_id"]) - req.setdefault("revision", self.state["revision"]) - if req.get("op") in {"input", "property", "run", "action", "save", "undo", "redo"}: - req.setdefault("selection", self.state.get("selection", [])) - response = exchange(self.descriptor, req) - deadline = time.monotonic() + min(max(wait_seconds, 0), 60) - while response.get("status") in ("accepted", "running") and time.monotonic() < deadline: - time.sleep(0.05) - response = exchange(self.descriptor, {"op": "operation", "request_id": req["request_id"]}) - if "state" in response: - self.state = response["state"] - elif req.get("op") in ("hello", "state") and response.get("ok"): - self.state = response - return response - - -def launch(binary: str | None = None) -> list[dict]: - existing = sessions() - if existing: - return existing - executable = binary or os.environ.get("OCS_BINARY", str(Path.home() / ".local/share/ocs-control/OpenCADStudio")) - log_dir = Path.home() / ".local/share/ocs-control" - log_dir.mkdir(parents=True, exist_ok=True) - with (log_dir / "gui.log").open("ab") as log: - proc = subprocess.Popen([executable, "--new-instance"], stdin=subprocess.DEVNULL, stdout=log, stderr=log, start_new_session=True) - deadline = time.monotonic() + 30 - while time.monotonic() < deadline: - if proc.poll() is not None: - raise RuntimeError(f"OCS exited ({proc.returncode}); inspect {log_dir / 'gui.log'}") - available = sessions() - if available: - return available - time.sleep(0.2) - raise TimeoutError("OCS is still starting; call sessions again") - - -if __name__ == "__main__": - import argparse - parser = argparse.ArgumentParser() - parser.add_argument("request", nargs="?", help="JSON request; omit to list sessions") - parser.add_argument("--session") - parser.add_argument("--launch", action="store_true") - args = parser.parse_args() - if args.launch: - launch() - if args.request: - print(json.dumps(LiveOcs(args.session).request(json.loads(args.request)), ensure_ascii=False)) - else: - print(json.dumps([s["state"] for s in sessions()], ensure_ascii=False)) diff --git a/docs/automation/ocs.py b/docs/automation/ocs.py deleted file mode 100644 index 5b6b98987..000000000 --- a/docs/automation/ocs.py +++ /dev/null @@ -1,153 +0,0 @@ -"""Thin Python client for the Open CAD Studio headless automation server. - -Launches `OpenCADStudio --serve` and talks to it over a line-based JSON protocol -(one request object per line on stdin, one response per line on stdout). There -is nothing to compile or maintain on the Python side — every method is one JSON -message; the real work is Open CAD Studio's own command system. - - from ocs import Ocs - - with Ocs(binary="OpenCADStudio") as ocs: - ocs.open("plan.dwg") - ocs.run("LAYER Walls") - print(ocs.entities()) # {"total": 42, "by_type": {...}} - ocs.save("plan_out.dwg") - -Each call returns the parsed response dict and raises `OcsError` on `ok: false`. -""" - -from __future__ import annotations - -import json -import socket -import subprocess -from typing import Any, Optional - - -class OcsError(RuntimeError): - """Raised when the server replies with `{"ok": false, ...}`.""" - - -class Ocs: - """Connect by spawning the server (default) or over a TCP socket. - - - `Ocs()` spawns `OpenCADStudio --serve` and talks over stdin/stdout. - - `Ocs(port=4242)` connects to a server started with `--serve --port 4242`. - """ - - def __init__( - self, - binary: str = "OpenCADStudio", - port: Optional[int] = None, - host: str = "127.0.0.1", - ) -> None: - self.proc: Optional[subprocess.Popen] = None - self.sock: Optional[socket.socket] = None - if port is not None: - self.sock = socket.create_connection((host, port)) - io = self.sock.makefile("rw") - self._r, self._w = io, io - else: - self.proc = subprocess.Popen( - [binary, "--serve"], - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - text=True, - bufsize=1, - ) - self._r, self._w = self.proc.stdout, self.proc.stdin - self._read() # the {"ready": true} greeting - - # ── protocol ──────────────────────────────────────────────────────────── - def _read(self) -> dict[str, Any]: - line = self._r.readline() - if not line: - raise OcsError("server closed the connection") - return json.loads(line) - - def _send(self, **req: Any) -> dict[str, Any]: - self._w.write(json.dumps(req) + "\n") - self._w.flush() - resp = self._read() - if not resp.get("ok", False): - raise OcsError(resp.get("error", "unknown error")) - return resp - - # ── operations ────────────────────────────────────────────────────────── - def new(self) -> dict[str, Any]: - """Start an empty document.""" - return self._send(op="new") - - def open(self, path: str) -> dict[str, Any]: - """Load a DWG/DXF drawing.""" - return self._send(op="open", path=path) - - def run(self, cmd: str) -> dict[str, Any]: - """Run a command through Open CAD Studio's command system.""" - return self._send(op="run", cmd=cmd) - - def entities(self) -> dict[str, Any]: - """Total entity count and a breakdown by type.""" - return self._send(op="entities") - - def query( - self, - type: Optional[str] = None, - layer: Optional[str] = None, - limit: Optional[int] = None, - ) -> dict[str, Any]: - """List entities (handle, type, layer, geometry), optionally filtered.""" - return self._send(op="query", type=type, layer=layer, limit=limit) - - def layers(self) -> dict[str, Any]: - """List layers (name, color, on/off, frozen, locked) and the current one.""" - return self._send(op="layers") - - def header(self) -> dict[str, Any]: - """Read drawing header variables (units, PDMODE/PDSIZE, LTSCALE, …).""" - return self._send(op="header") - - def select( - self, - handles: Optional[list[str]] = None, - type: Optional[str] = None, - layer: Optional[str] = None, - clear: bool = False, - ) -> dict[str, Any]: - """Set the selection by handle, type, or layer (a following selection - command like ``run("ERASE")`` then acts on it). `clear=True` deselects.""" - return self._send( - op="select", handles=handles, type=type, layer=layer, clear=clear - ) - - def undo(self) -> dict[str, Any]: - """Undo the last change.""" - return self._send(op="undo") - - def redo(self) -> dict[str, Any]: - """Redo the last undone change.""" - return self._send(op="redo") - - def save(self, path: Optional[str] = None) -> dict[str, Any]: - """Write the document (defaults to the opened/last-saved path).""" - return self._send(op="save", path=path) - - # ── lifecycle ─────────────────────────────────────────────────────────── - def close(self) -> None: - try: - self._w.close() - except Exception: - pass - if self.proc is not None: - try: - self.proc.wait(timeout=5) - except subprocess.TimeoutExpired: - self.proc.kill() - if self.sock is not None: - self.sock.close() - - def __enter__(self) -> "Ocs": - return self - - def __exit__(self, *_exc: object) -> None: - self.close() diff --git a/docs/readme/README.ar.md b/docs/readme/README.ar.md index c9e70d2f8..eeb10743b 100644 --- a/docs/readme/README.ar.md +++ b/docs/readme/README.ar.md @@ -149,9 +149,10 @@ trunk serve OpenCADStudio --export input.dwg output.dxf OpenCADStudio --serve OpenCADStudio --serve --port 4242 +OpenCADStudio --mcp ``` -يتبادل الخادم كائن JSON واحداً في كل سطر عبر الإدخال/الإخراج القياسي أو مقبس TCP محلي. راجع [دليل الأتمتة](../automation/README.md) و[عميل Python](../automation/ocs.py) المرفق. +يتبادل الخادم كائن JSON واحداً في كل سطر عبر الإدخال/الإخراج القياسي أو مقبس TCP محلي. راجع [دليل الأتمتة](../automation/README.md). ## الإضافات diff --git a/docs/readme/README.bg.md b/docs/readme/README.bg.md index 9762e47ee..975f642b0 100644 --- a/docs/readme/README.bg.md +++ b/docs/readme/README.bg.md @@ -151,9 +151,10 @@ trunk serve OpenCADStudio --export input.dwg output.dxf OpenCADStudio --serve OpenCADStudio --serve --port 4242 +OpenCADStudio --mcp ``` -Сървърът обменя по един JSON обект на ред чрез стандартния вход/изход или локален TCP сокет. Вижте [ръководството за автоматизация](../automation/README.md) и включения [Python клиент](../automation/ocs.py). +Сървърът обменя по един JSON обект на ред чрез стандартния вход/изход или локален TCP сокет. Вижте [ръководството за автоматизация](../automation/README.md). ## Приставки diff --git a/docs/readme/README.cs.md b/docs/readme/README.cs.md index 7fd0758ad..be39bd983 100644 --- a/docs/readme/README.cs.md +++ b/docs/readme/README.cs.md @@ -149,9 +149,10 @@ Desktopový program podporuje jednorázovou konverzi a trvalý bezobslužný ser OpenCADStudio --export input.dwg output.dxf OpenCADStudio --serve OpenCADStudio --serve --port 4242 +OpenCADStudio --mcp ``` -Server předává jeden objekt JSON na řádek přes standardní vstup/výstup nebo místní TCP socket. Viz [průvodce automatizací](../automation/README.md) a přiložený [klient Python](../automation/ocs.py). +Server předává jeden objekt JSON na řádek přes standardní vstup/výstup nebo místní TCP socket. Viz [průvodce automatizací](../automation/README.md). ## Pluginy diff --git a/docs/readme/README.de.md b/docs/readme/README.de.md index 3deabd159..4c7d8a979 100644 --- a/docs/readme/README.de.md +++ b/docs/readme/README.de.md @@ -149,9 +149,10 @@ Die Desktop-Binärdatei unterstützt einmalige Konvertierung und einen dauerhaft OpenCADStudio --export input.dwg output.dxf OpenCADStudio --serve OpenCADStudio --serve --port 4242 +OpenCADStudio --mcp ``` -Der Server tauscht über Standardein-/ausgabe oder einen lokalen TCP-Socket je Zeile ein JSON-Objekt aus. Siehe [Automatisierungsanleitung](../automation/README.md) und den enthaltenen [Python-Client](../automation/ocs.py). +Der Server tauscht über Standardein-/ausgabe oder einen lokalen TCP-Socket je Zeile ein JSON-Objekt aus. Siehe [Automatisierungsanleitung](../automation/README.md). ## Plugins diff --git a/docs/readme/README.es.md b/docs/readme/README.es.md index 9c7a09e39..2d0e937bc 100644 --- a/docs/readme/README.es.md +++ b/docs/readme/README.es.md @@ -149,9 +149,10 @@ El binario de escritorio admite conversión puntual y un servidor persistente si OpenCADStudio --export input.dwg output.dxf OpenCADStudio --serve OpenCADStudio --serve --port 4242 +OpenCADStudio --mcp ``` -El servidor intercambia un objeto JSON por línea mediante entrada/salida estándar o un socket TCP local. Consulta la [guía de automatización](../automation/README.md) y el [cliente Python](../automation/ocs.py) incluido. +El servidor intercambia un objeto JSON por línea mediante entrada/salida estándar o un socket TCP local. Consulta la [guía de automatización](../automation/README.md). ## Complementos diff --git a/docs/readme/README.fi.md b/docs/readme/README.fi.md index 800c9d18a..b4badeb71 100644 --- a/docs/readme/README.fi.md +++ b/docs/readme/README.fi.md @@ -149,9 +149,10 @@ Työpöytäohjelma tukee kertaluonteista muunnosta ja pysyvää käyttöliittym OpenCADStudio --export input.dwg output.dxf OpenCADStudio --serve OpenCADStudio --serve --port 4242 +OpenCADStudio --mcp ``` -Palvelin vaihtaa yhden JSON-objektin riviä kohti vakiotulon/-lähdön tai paikallisen TCP-pistokkeen kautta. Katso [automaatio-opas](../automation/README.md) ja mukana toimitettu [Python-asiakas](../automation/ocs.py). +Palvelin vaihtaa yhden JSON-objektin riviä kohti vakiotulon/-lähdön tai paikallisen TCP-pistokkeen kautta. Katso [automaatio-opas](../automation/README.md). ## Liitännäiset diff --git a/docs/readme/README.fr.md b/docs/readme/README.fr.md index 14e32d3c3..2371fec90 100644 --- a/docs/readme/README.fr.md +++ b/docs/readme/README.fr.md @@ -149,9 +149,10 @@ Le binaire de bureau prend en charge la conversion ponctuelle et un serveur pers OpenCADStudio --export input.dwg output.dxf OpenCADStudio --serve OpenCADStudio --serve --port 4242 +OpenCADStudio --mcp ``` -Le serveur échange un objet JSON par ligne via l’entrée/sortie standard ou un socket TCP local. Consultez le [guide d’automatisation](../automation/README.md) et le [client Python](../automation/ocs.py) fourni. +Le serveur échange un objet JSON par ligne via l’entrée/sortie standard ou un socket TCP local. Consultez le [guide d’automatisation](../automation/README.md). ## Plugins diff --git a/docs/readme/README.hi.md b/docs/readme/README.hi.md index fe46322ff..67d7cd94f 100644 --- a/docs/readme/README.hi.md +++ b/docs/readme/README.hi.md @@ -149,9 +149,10 @@ trunk serve OpenCADStudio --export input.dwg output.dxf OpenCADStudio --serve OpenCADStudio --serve --port 4242 +OpenCADStudio --mcp ``` -सर्वर मानक इनपुट/आउटपुट या स्थानीय TCP सॉकेट के माध्यम से प्रति पंक्ति एक JSON ऑब्जेक्ट का आदान-प्रदान करता है। [स्वचालन मार्गदर्शिका](../automation/README.md) और शामिल [Python क्लाइंट](../automation/ocs.py) देखें। +सर्वर मानक इनपुट/आउटपुट या स्थानीय TCP सॉकेट के माध्यम से प्रति पंक्ति एक JSON ऑब्जेक्ट का आदान-प्रदान करता है। [स्वचालन मार्गदर्शिका](../automation/README.md) देखें। ## प्लगइन diff --git a/docs/readme/README.hu.md b/docs/readme/README.hu.md index 803c56da3..655315564 100644 --- a/docs/readme/README.hu.md +++ b/docs/readme/README.hu.md @@ -149,9 +149,10 @@ Az asztali bináris egyszeri konverziót és tartós, felület nélküli kiszolg OpenCADStudio --export input.dwg output.dxf OpenCADStudio --serve OpenCADStudio --serve --port 4242 +OpenCADStudio --mcp ``` -A kiszolgáló soronként egy JSON-objektumot cserél a szabványos bemeneten/kimeneten vagy helyi TCP-foglalaton. Lásd az [automatizálási útmutatót](../automation/README.md) és a mellékelt [Python-klienst](../automation/ocs.py). +A kiszolgáló soronként egy JSON-objektumot cserél a szabványos bemeneten/kimeneten vagy helyi TCP-foglalaton. Lásd az [automatizálási útmutatót](../automation/README.md). ## Bővítmények diff --git a/docs/readme/README.it.md b/docs/readme/README.it.md index ea727d5ef..e342cf765 100644 --- a/docs/readme/README.it.md +++ b/docs/readme/README.it.md @@ -149,9 +149,10 @@ Il binario desktop supporta conversioni singole e un server headless persistente OpenCADStudio --export input.dwg output.dxf OpenCADStudio --serve OpenCADStudio --serve --port 4242 +OpenCADStudio --mcp ``` -Il server scambia un oggetto JSON per riga tramite ingresso/uscita standard o un socket TCP locale. Consulta la [guida all’automazione](../automation/README.md) e il [client Python](../automation/ocs.py) incluso. +Il server scambia un oggetto JSON per riga tramite ingresso/uscita standard o un socket TCP locale. Consulta la [guida all’automazione](../automation/README.md). ## Plugin diff --git a/docs/readme/README.ja.md b/docs/readme/README.ja.md index d81babe52..2fbba316a 100644 --- a/docs/readme/README.ja.md +++ b/docs/readme/README.ja.md @@ -149,9 +149,10 @@ trunk serve OpenCADStudio --export input.dwg output.dxf OpenCADStudio --serve OpenCADStudio --serve --port 4242 +OpenCADStudio --mcp ``` -サーバーは標準入出力またはローカル TCP ソケットを通じ、1 行につき 1 個の JSON オブジェクトを交換します。[自動化ガイド](../automation/README.md) と付属の [Python クライアント](../automation/ocs.py) を参照してください。 +サーバーは標準入出力またはローカル TCP ソケットを通じ、1 行につき 1 個の JSON オブジェクトを交換します。[自動化ガイド](../automation/README.md) を参照してください。 ## プラグイン diff --git a/docs/readme/README.ko.md b/docs/readme/README.ko.md index 563757704..ded42890c 100644 --- a/docs/readme/README.ko.md +++ b/docs/readme/README.ko.md @@ -149,9 +149,10 @@ trunk serve OpenCADStudio --export input.dwg output.dxf OpenCADStudio --serve OpenCADStudio --serve --port 4242 +OpenCADStudio --mcp ``` -서버는 표준 입출력 또는 로컬 TCP 소켓을 통해 한 줄에 하나의 JSON 객체를 교환합니다. [자동화 안내서](../automation/README.md)와 포함된 [Python 클라이언트](../automation/ocs.py)를 참고하세요. +서버는 표준 입출력 또는 로컬 TCP 소켓을 통해 한 줄에 하나의 JSON 객체를 교환합니다. [자동화 안내서](../automation/README.md)를 참고하세요. ## 플러그인 diff --git a/docs/readme/README.nl.md b/docs/readme/README.nl.md index 8c03857bc..7d2e4156d 100644 --- a/docs/readme/README.nl.md +++ b/docs/readme/README.nl.md @@ -149,9 +149,10 @@ Het desktopprogramma ondersteunt eenmalige conversie en een permanente headless OpenCADStudio --export input.dwg output.dxf OpenCADStudio --serve OpenCADStudio --serve --port 4242 +OpenCADStudio --mcp ``` -De server wisselt per regel één JSON-object uit via standaardinvoer/-uitvoer of een lokale TCP-socket. Bekijk de [automatiseringshandleiding](../automation/README.md) en de meegeleverde [Python-client](../automation/ocs.py). +De server wisselt per regel één JSON-object uit via standaardinvoer/-uitvoer of een lokale TCP-socket. Bekijk de [automatiseringshandleiding](../automation/README.md). ## Plug-ins diff --git a/docs/readme/README.pl.md b/docs/readme/README.pl.md index 9d154aa8c..680881a89 100644 --- a/docs/readme/README.pl.md +++ b/docs/readme/README.pl.md @@ -149,9 +149,10 @@ Program komputerowy obsługuje jednorazową konwersję i trwały serwer bez inte OpenCADStudio --export input.dwg output.dxf OpenCADStudio --serve OpenCADStudio --serve --port 4242 +OpenCADStudio --mcp ``` -Serwer wymienia jeden obiekt JSON na wiersz przez standardowe wejście/wyjście lub lokalne gniazdo TCP. Zobacz [przewodnik automatyzacji](../automation/README.md) i dołączonego [klienta Python](../automation/ocs.py). +Serwer wymienia jeden obiekt JSON na wiersz przez standardowe wejście/wyjście lub lokalne gniazdo TCP. Zobacz [przewodnik automatyzacji](../automation/README.md). ## Wtyczki diff --git a/docs/readme/README.pt-BR.md b/docs/readme/README.pt-BR.md index 267100eab..bb7824322 100644 --- a/docs/readme/README.pt-BR.md +++ b/docs/readme/README.pt-BR.md @@ -149,9 +149,10 @@ O binário desktop oferece conversão única e um servidor persistente sem inter OpenCADStudio --export input.dwg output.dxf OpenCADStudio --serve OpenCADStudio --serve --port 4242 +OpenCADStudio --mcp ``` -O servidor troca um objeto JSON por linha pela entrada/saída padrão ou por um socket TCP local. Consulte o [guia de automação](../automation/README.md) e o [cliente Python](../automation/ocs.py) incluído. +O servidor troca um objeto JSON por linha pela entrada/saída padrão ou por um socket TCP local. Consulte o [guia de automação](../automation/README.md). ## Plugins diff --git a/docs/readme/README.ru.md b/docs/readme/README.ru.md index da2a25b51..5b2d178f7 100644 --- a/docs/readme/README.ru.md +++ b/docs/readme/README.ru.md @@ -149,9 +149,10 @@ trunk serve OpenCADStudio --export input.dwg output.dxf OpenCADStudio --serve OpenCADStudio --serve --port 4242 +OpenCADStudio --mcp ``` -Сервер обменивается одним объектом JSON в строке через стандартный ввод/вывод или локальный TCP-сокет. См. [руководство по автоматизации](../automation/README.md) и включённый [клиент Python](../automation/ocs.py). +Сервер обменивается одним объектом JSON в строке через стандартный ввод/вывод или локальный TCP-сокет. См. [руководство по автоматизации](../automation/README.md). ## Плагины diff --git a/docs/readme/README.tr.md b/docs/readme/README.tr.md index 6086fe56d..52c760b6b 100644 --- a/docs/readme/README.tr.md +++ b/docs/readme/README.tr.md @@ -151,9 +151,10 @@ Masaüstü ikili dosyası tek seferlik dönüştürmeyi ve kalıcı arayüzsüz OpenCADStudio --export input.dwg output.dxf OpenCADStudio --serve OpenCADStudio --serve --port 4242 +OpenCADStudio --mcp ``` -Sunucu standart giriş/çıkış veya yerel TCP soketi üzerinden satır başına bir JSON nesnesi alışverişi yapar. [Otomasyon kılavuzuna](../automation/README.md) ve birlikte verilen [Python istemcisine](../automation/ocs.py) bakın. +Sunucu standart giriş/çıkış veya yerel TCP soketi üzerinden satır başına bir JSON nesnesi alışverişi yapar. [Otomasyon kılavuzuna](../automation/README.md) bakın. ## Eklentiler diff --git a/docs/readme/README.zh-CN.md b/docs/readme/README.zh-CN.md index 49c526e01..901bd350b 100644 --- a/docs/readme/README.zh-CN.md +++ b/docs/readme/README.zh-CN.md @@ -149,9 +149,10 @@ trunk serve OpenCADStudio --export input.dwg output.dxf OpenCADStudio --serve OpenCADStudio --serve --port 4242 +OpenCADStudio --mcp ``` -服务器通过标准输入/输出或本地 TCP 套接字逐行交换 JSON 对象。请参阅[自动化指南](../automation/README.md)和随附的 [Python 客户端](../automation/ocs.py)。 +服务器通过标准输入/输出或本地 TCP 套接字逐行交换 JSON 对象。请参阅[自动化指南](../automation/README.md)。 ## 插件 diff --git a/docs/readme/README.zh-TW.md b/docs/readme/README.zh-TW.md index dcf57282b..8d9f2f21f 100644 --- a/docs/readme/README.zh-TW.md +++ b/docs/readme/README.zh-TW.md @@ -149,9 +149,10 @@ trunk serve OpenCADStudio --export input.dwg output.dxf OpenCADStudio --serve OpenCADStudio --serve --port 4242 +OpenCADStudio --mcp ``` -伺服器透過標準輸入/輸出或本機 TCP 通訊端逐行交換 JSON 物件。請參閱[自動化指南](../automation/README.md)和隨附的 [Python 用戶端](../automation/ocs.py)。 +伺服器透過標準輸入/輸出或本機 TCP 通訊端逐行交換 JSON 物件。請參閱[自動化指南](../automation/README.md)。 ## 外掛程式 diff --git a/integrations/codex/opencadstudio-control/SKILL.md b/integrations/codex/opencadstudio-control/SKILL.md deleted file mode 100644 index 1c4b3831c..000000000 --- a/integrations/codex/opencadstudio-control/SKILL.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: opencadstudio-control -description: Inspect and control the user's live OpenCADStudio desktop session through its persistent MCP tools. Use for drawing, editing, selecting, measuring, changing properties or layers, opening/saving/exporting files, reviewing the current OCS UI, or visually verifying OCS results. Do not use for source-code changes to the OpenCADStudio repository. ---- - -# OpenCADStudio Control - -Use the `ocs_*` MCP tools to act on the actual running application. Call `ocs_sessions` first; it opens the installed application when none is running. If multiple sessions exist, select the intended `session_id` from the listed documents instead of guessing. - -Read `state` before an edit. Carry its `session_id`, `document_id`, `revision`, and selection into mutations. Treat `request_id` as the idempotency key: after a timeout, query that operation and never replay it with a new ID until its absence is established. Refresh state after `stale_state`, `selection_changed`, `document_closed`, or `session_changed`. - -Prefer semantic commands and inputs: - -- Use `start` followed by typed `input` calls for interactive geometry, points, entity handles, structure picks, selection completion, text, Enter, and cancellation. -- Use `run` for a complete command line, and respect `waiting_input`; inspect its prompt and language-independent option keywords before continuing. -- Use `property` with IDs returned by `properties`, and `action` with IDs returned by `commands`. -- Let OCS and its geometry kernel calculate geometry. Verify important results with entity/document queries and a current `ocs_capture` image. -- Save to an explicit path, wait for `completed`, reopen when persistence or round-trip correctness matters, and inspect state afterward. - -Keep user edits and open tabs intact unless the task requires changing them. A successful transport response can still report `waiting_input`, `failed`, `cancelled`, or a running operation; only `completed` means that operation finished. diff --git a/integrations/codex/opencadstudio-control/agents/openai.yaml b/integrations/codex/opencadstudio-control/agents/openai.yaml deleted file mode 100644 index 4ae375464..000000000 --- a/integrations/codex/opencadstudio-control/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "OpenCADStudio Control" - short_description: "Control and inspect the live OpenCADStudio GUI" - default_prompt: "Use the live OpenCADStudio session to complete this CAD task and verify the result." diff --git a/packaging/mcpb/pack.py b/packaging/mcpb/pack.py new file mode 100644 index 000000000..ef412a6ae --- /dev/null +++ b/packaging/mcpb/pack.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +"""Build the client-neutral OpenCADStudio MCP Bundle and registry metadata.""" + +import argparse +import hashlib +import json +from pathlib import Path +import stat +import zipfile + + +def manifest(version: str) -> dict: + return { + "manifest_version": "0.3", + "name": "opencadstudio", + "display_name": "OpenCADStudio", + "version": version, + "description": "Control and inspect live OpenCADStudio drawings through MCP", + "long_description": "Opens OpenCADStudio when needed and provides semantic drawing commands, document inspection, measurements, and visual verification.", + "author": { + "name": "Hakan Seven", + "url": "https://github.com/HakanSeven12/OpenCADStudio", + }, + "repository": { + "type": "git", + "url": "https://github.com/HakanSeven12/OpenCADStudio", + }, + "server": { + "type": "binary", + "entry_point": "server/linux-x86_64/OpenCADStudio", + "mcp_config": { + "command": "${__dirname}/server/linux-x86_64/OpenCADStudio", + "args": ["--mcp"], + "env": {}, + "platform_overrides": { + "win32": { + "command": "${__dirname}/server/win32-x86_64/OpenCADStudio.exe" + }, + "darwin": { + "command": "${__dirname}/server/darwin-aarch64/OpenCADStudio" + }, + }, + }, + }, + "tools": [ + {"name": "ocs_sessions", "description": "List or open live editor sessions"}, + {"name": "ocs_read", "description": "Read drawing and editor state"}, + {"name": "ocs_execute", "description": "Execute a semantic editor action"}, + {"name": "ocs_capture", "description": "Capture the current editor window"}, + ], + "keywords": ["cad", "drawing", "mcp"], + "license": "GPL-3.0-or-later", + "compatibility": {"platforms": ["linux", "win32", "darwin"]}, + } + + +def add(archive: zipfile.ZipFile, source: Path, destination: str) -> None: + if not source.is_file(): + raise SystemExit(f"Missing MCP binary: {source}") + info = zipfile.ZipInfo(destination, date_time=(1980, 1, 1, 0, 0, 0)) + info.compress_type = zipfile.ZIP_DEFLATED + info.create_system = 3 + info.external_attr = (stat.S_IFREG | 0o755) << 16 + archive.writestr(info, source.read_bytes()) + + +def registry_metadata(version: str, bundle: Path) -> dict: + digest = hashlib.sha256(bundle.read_bytes()).hexdigest() + return { + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "name": "io.github.HakanSeven12/opencadstudio", + "title": "OpenCADStudio", + "description": "Control and inspect live OpenCADStudio drawings through MCP", + "version": version, + "repository": { + "url": "https://github.com/HakanSeven12/OpenCADStudio", + "source": "github", + }, + "websiteUrl": "https://github.com/HakanSeven12/OpenCADStudio", + "packages": [ + { + "registryType": "mcpb", + "identifier": f"https://github.com/HakanSeven12/OpenCADStudio/releases/download/v{version}/{bundle.name}", + "version": version, + "fileSha256": digest, + "transport": {"type": "stdio"}, + } + ], + } + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--version", required=True) + parser.add_argument("--linux", type=Path, required=True) + parser.add_argument("--windows", type=Path, required=True) + parser.add_argument("--macos", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--server-json", type=Path, required=True) + args = parser.parse_args() + + args.output.parent.mkdir(parents=True, exist_ok=True) + with zipfile.ZipFile(args.output, "w") as archive: + archive.writestr("manifest.json", json.dumps(manifest(args.version), indent=2) + "\n") + add(archive, args.linux, "server/linux-x86_64/OpenCADStudio") + add(archive, args.windows, "server/win32-x86_64/OpenCADStudio.exe") + add(archive, args.macos, "server/darwin-aarch64/OpenCADStudio") + + args.server_json.parent.mkdir(parents=True, exist_ok=True) + args.server_json.write_text( + json.dumps(registry_metadata(args.version, args.output), indent=2) + "\n", + encoding="utf-8", + ) + + +if __name__ == "__main__": + main()