Ship client-neutral MCP bundles

This commit is contained in:
Hakan Seven committed 2026-09-04 23:58:39 +03:00
1 parent 17e5cda3fb
commit afbf826b0c
30 files changed
+285 -446

No files matched your search

+3 -2
View File
@@ -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:
+112
View File
@@ -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
+1 -1
View File
@@ -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
+14 -33
View File
@@ -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
```
-65
View File
@@ -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
-19
View File
@@ -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'
-130
View File
@@ -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))
-153
View File
@@ -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()
+2 -1
View File
@@ -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).
## الإضافات
+2 -1
View File
@@ -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).
## Приставки
+2 -1
View File
@@ -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
+2 -1
View File
@@ -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
+2 -1
View File
@@ -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
+2 -1
View File
@@ -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
+2 -1
View File
@@ -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
+2 -1
View File
@@ -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) देखें।
## प्लगइन
+2 -1
View File
@@ -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
+2 -1
View File
@@ -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
+2 -1
View File
@@ -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) を参照してください。
## プラグイン
+2 -1
View File
@@ -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)를 참고하세요.
## 플러그인
+2 -1
View File
@@ -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
+2 -1
View File
@@ -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
+2 -1
View File
@@ -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
+2 -1
View File
@@ -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).
## Плагины
+2 -1
View File
@@ -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
+2 -1
View File
@@ -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)。
## 插件
+2 -1
View File
@@ -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)。
## 外掛程式
@@ -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.
@@ -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."
+117
View File
@@ -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()