forked from 1b-agent/opencadstudio-upstream
Ship client-neutral MCP bundles
This commit is contained in:
1 parent
17e5cda3fb
commit
afbf826b0c
30 files changed
+285
-446
No files matched your search
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
@@ -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'
|
||||
@@ -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))
|
||||
@@ -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()
|
||||
@@ -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).
|
||||
|
||||
## الإضافات
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
## Приставки
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) देखें।
|
||||
|
||||
## प्लगइन
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) を参照してください。
|
||||
|
||||
## プラグイン
|
||||
|
||||
|
||||
@@ -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)를 참고하세요.
|
||||
|
||||
## 플러그인
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
## Плагины
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)。
|
||||
|
||||
## 插件
|
||||
|
||||
|
||||
@@ -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."
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user