Files
OneCAD-Draft/docs/automation/mcp_smoke.py
T
huanldandClaude Opus 5.5 279a87418e feat(automation): entity, block, publishing and REST operations from the fork
The operations docs/automation/API-SPEC.md (PR #1383) describes, brought
over from huaninstratech/OpenCADStudio so the specification no longer runs
ahead of the code:

- entity CRUD: entities_create, entities_delete, entities_transform,
  entities_copy_to, and `where` filters on query
- blocks and export: block_define, block_delete, a normalised wblock
- publishing: layout_create, page_setup_set, plot (per_page included)
- metadata: sysvar, xdata_set / xdata_get, file_identity
- collections: group_create, selection_set_save / load
- control plane: batch, events, view_focus, close
- the REST transport: `--http <port>`, the /api/v1 routes and their
  OpenAPI 3 description
- person-in-the-loop picks: user_select and getpoint, hosted by the GUI
  over the same REST routes, with on-screen pending signals

Taken from the fork's commits b7a7ed80, 69396b42, 64f1cf6a, c3aafc8e,
bb5a7bfa, 52317a5c, 50d293ba, b90e194f, 517c6397, 53a5c4c9, a49ac045,
37de5a46, a78b1822, 6d9aed2a, 33f522da, 5b5cafe2, 02a7e78d, 94df40dc,
fd5bc14d, 485fac61 and 2c5fc994.

Left out on purpose: 37de5a46 also removed the donation surface (the
Patreon fetch, the donation prompt, the Start-page supporters rail, the
DONATE command and its locale strings) and replaced the About dialog's call
to action; that is the fork's own product decision and stays out. The
fork's version bump and its unrelated IFC/BIM work are not part of this.

Adapted while porting: the REST server and the GUI bridge are desktop-only
and a template start is refused in the web build, so the wasm target still
compiles; the two plot tests read the PDF through the shared stream helper,
because exported streams are Flate-compressed; the benches set the new
crosshair field; comments and docs describe each operation in its own
terms rather than by another product's API names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 13:41:07 +03:00

166 lines
6.3 KiB
Python

"""Protocol smoke test for the native client-neutral MCP endpoint."""
import json
from pathlib import Path
import subprocess
import sys
TOOLS = {"ocs_sessions", "ocs_read", "ocs_execute", "ocs_capture"}
MODERN = "2026-07-28"
def start(server: Path) -> subprocess.Popen[str]:
process = subprocess.Popen(
[str(server), "--mcp"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
assert process.stdin and process.stdout
return process
def request(process: subprocess.Popen[str], payload: dict) -> dict:
assert process.stdin and process.stdout
process.stdin.write(json.dumps(payload) + "\n")
process.stdin.flush()
return json.loads(process.stdout.readline())
def close(process: subprocess.Popen[str]) -> None:
assert process.stdin
process.stdin.close()
assert process.wait(timeout=5) == 0
def legacy(server: Path) -> None:
process = start(server)
initialized = request(process, {
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {"protocolVersion": "2025-11-25", "capabilities": {}, "clientInfo": {"name": "smoke", "version": "1"}},
})
assert initialized["result"]["serverInfo"]["name"] == "OpenCADStudio"
assert "state.command.accepts" in initialized["result"]["instructions"]
process.stdin.write(json.dumps({"jsonrpc": "2.0", "method": "notifications/initialized"}) + "\n")
process.stdin.flush()
tools = request(process, {"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}})
definitions = {tool["name"]: tool for tool in tools["result"]["tools"]}
names = set(definitions)
assert names == TOOLS, names
execute_request = definitions["ocs_execute"]["inputSchema"]["properties"]["request"]
assert len(execute_request["oneOf"]) == 35
op_enum = execute_request["properties"]["op"]["enum"]
for shipped in ("entities_create", "entities_delete", "entities_transform",
"block_define", "block_delete", "file_identity", "xdata_set",
"view_focus", "wblock", "plot",
"entities_copy_to", "group_create", "selection_set_save",
"selection_set_load", "close", "sysvar", "layout_create",
"page_setup_set"):
assert shipped in op_enum, shipped
assert "xdata_get" in definitions["ocs_read"]["inputSchema"]["properties"]["op"]["enum"]
assert execute_request["properties"]["steps"]["maxItems"] == 64
assert execute_request["properties"]["cmd"]["examples"][0] == "LINE 0,0 10,10"
assert "set_properties" in execute_request["properties"]["op"]["enum"]
assert "record_schema" in definitions["ocs_read"]["inputSchema"]["properties"]["op"]["enum"]
read_parameters = definitions["ocs_read"]["inputSchema"]["properties"]["parameters"]["properties"]
assert {"collection", "where", "paths"} <= set(read_parameters)
assert execute_request["properties"]["kind"]["enum"] == [
"text", "token", "point", "entity", "structure", "selection", "enter"
]
for name in {"ocs_sessions", "ocs_read", "ocs_execute"}:
assert definitions[name]["outputSchema"]["type"] == "object"
assert "resultType" not in tools["result"]
sessions = request(process, {
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {"name": "ocs_sessions", "arguments": {"launch_if_none": False}},
})
assert not sessions["result"].get("isError"), sessions
close(process)
def modern(server: Path) -> None:
process = start(server)
meta = {
"io.modelcontextprotocol/protocolVersion": MODERN,
"io.modelcontextprotocol/clientInfo": {"name": "smoke", "version": "1"},
"io.modelcontextprotocol/clientCapabilities": {},
}
discovered = request(process, {
"jsonrpc": "2.0",
"id": "discover",
"method": "server/discover",
"params": {"_meta": meta},
})
assert discovered["result"]["resultType"] == "complete"
assert discovered["result"]["ttlMs"] >= 0
assert discovered["result"]["cacheScope"] in {"public", "private"}
assert "io.modelcontextprotocol/tasks" in discovered["result"]["capabilities"]["extensions"]
tools = request(process, {
"jsonrpc": "2.0",
"id": "tools",
"method": "tools/list",
"params": {"_meta": meta},
})
assert {tool["name"] for tool in tools["result"]["tools"]} == TOOLS
assert tools["result"]["resultType"] == "complete"
assert tools["result"]["ttlMs"] >= 0
assert tools["result"]["cacheScope"] in {"public", "private"}
definitions = {tool["name"]: tool for tool in tools["result"]["tools"]}
assert definitions["ocs_execute"]["inputSchema"]["properties"]["response_detail"]["default"] == "compact"
assert definitions["ocs_capture"]["inputSchema"]["properties"]["scope"]["default"] == "viewport"
missing_cmd = request(process, {
"jsonrpc": "2.0",
"id": "missing-cmd",
"method": "tools/call",
"params": {
"name": "ocs_execute",
"arguments": {
"ocs_session_id": "missing",
"request": {"op": "run", "request_id": "run-1"},
},
"_meta": meta,
},
})
error = missing_cmd["result"]["structuredContent"]
assert error["code"] == "invalid_arguments"
assert "LINE 0,0 10,10" in error["error"]
invalid_mutation = request(process, {
"jsonrpc": "2.0",
"id": "invalid-mutation",
"method": "tools/call",
"params": {
"name": "ocs_execute",
"arguments": {"ocs_session_id": "missing", "request": {"op": "undo"}},
"_meta": meta,
},
})
assert invalid_mutation["result"]["isError"] is True
assert "request_id" in invalid_mutation["result"]["structuredContent"]["error"]
rejected = request(process, {
"jsonrpc": "2.0",
"id": "unsupported",
"method": "tools/list",
"params": {"_meta": {"io.modelcontextprotocol/protocolVersion": "2099-01-01"}},
})
assert rejected["error"]["code"] == -32022
close(process)
def main() -> None:
server = Path(sys.argv[1] if len(sys.argv) > 1 else "target/debug/OpenCADStudio").resolve()
legacy(server)
modern(server)
if __name__ == "__main__":
main()