Files
OneCAD-Simulation/docs/mcp.md
T
0899587127 MCP server: modelcraft-cli mcp (headless or bridged), read/write roots, end-to-end analysis over MCP (#48)
* io: read and write roots that confine an automation client's file access

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* engine: every file a command reads or writes is checked against the session's roots

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* engine: headless pictures of a session (model, geometry, deformed shape and contours)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* mcp: MCP server over stdio with headless and connected backends, roots and caps

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* cli: modelcraft-cli mcp [--connect ADDR | --headless] with automation read and write roots

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* mcp: end-to-end analysis of a corpus part over MCP (open, tet mesh, define, solve, probe, render)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* docs: MCP server (docs/mcp.md), Claude Code setup, roots; architecture, README, roadmap, attribution

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

---------

Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 22:18:08 -04:00

154 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ModelCraft and AI agents (MCP)
ModelCraft speaks the [Model Context Protocol](https://modelcontextprotocol.io) over stdio
(JSON-RPC 2.0, one message per line; protocol revisions 2025-06-18, 2025-03-26 and 2024-11-05).
An agent builds and solves models the way a person does: every tool runs engine commands, the
same ones the UI, scripts, the command line and the control channel run, and never opens a
dialog or a file picker. The server is `crates/mcp` (`modelcraft-mcp`); `modelcraft-cli mcp`
runs it.
## Start it
```sh
# Headless (the default): an in-process session, no window; screenshots are CPU renders.
modelcraft-cli mcp
modelcraft-cli mcp --headless
# Bridged to the running app, so you can watch the agent work:
modelcraft --control 7070 &
modelcraft-cli mcp --connect 127.0.0.1:7070
```
With `--connect`, an app that cannot be reached when the server starts gives a headless session
instead (a note goes to stderr). stdout carries only protocol messages; errors go to stderr.
Bridged screenshots are written to a temporary file by the app and read back by the server, so
both must run on the same computer.
### Claude Code
```sh
claude mcp add modelcraft -- /path/to/modelcraft-cli mcp
# bridged to a running app:
claude mcp add modelcraft -- /path/to/modelcraft-cli mcp --connect 127.0.0.1:7070
# kept to one project folder:
claude mcp add modelcraft -- /path/to/modelcraft-cli mcp --automation-read-root /work/bracket --automation-write-root /work/bracket/out
```
Build the binary first (`cargo build --release -p modelcraft-cli`; it is
`target/release/modelcraft-cli`). The same setup as a project `.mcp.json`, or in another client's
configuration (Claude Desktop's `claude_desktop_config.json`):
```json
{
"mcpServers": {
"modelcraft": { "command": "/path/to/modelcraft-cli", "args": ["mcp"] }
}
}
```
## Confining file access
```sh
modelcraft-cli mcp --automation-read-root /work/bracket --automation-write-root /work/bracket/out
```
The two flags keep the agent to the folders you give it:
- Every file a command reads must lie inside the **read root**: `open`, `file.open`,
`geometry.open`, `geometry.import.mesh`, the design and meshes a `.modelcraft` file opens
again, `file.recovery.list` and `file.recover` with a `dir`.
- Every file a command writes must lie inside the **write root**: `save`, `file.save` and
`file.save_as` (also a save to the model's own file), `write_deck` and `deck.write`,
`screenshot {path}`, `file.recovery.discard` with a `dir`.
- Read and write access are separate. Give the same folder twice for both; a root left out
grants none of its access (with only `--automation-read-root`, nothing can be written). Without
either flag nothing is confined.
- A path is made absolute against the server's working folder, then its links are followed: a
file (or folder) link that leads out of a root is outside it. For a file that does not exist
yet, its nearest existing folder's links are followed and the rest must be plain names, so
`..` below a folder that does not exist is refused, as is a link that leads nowhere (writing
through it would create its target). The result is compared with the root by whole folder
names: `/work2/x` is not inside `/work`. On Windows device names (`CON`, `NUL`, `COM1`…) are
refused.
- A refused path is an ordinary tool error the agent can read and correct, for example
`automation path rejected: outside the write root /work/bracket/out: /etc/x.inp` or
`automation filesystem access is not granted: write authority is absent: /work/bracket/out/x.inp`.
A design or mesh a model refers to outside the read root is not opened; the model opens
without it and says so in `warnings`.
- The app's own recovery folder (autosaves, `file.recovery.*` without `dir`) is not confined:
no agent names a path there.
- The roots must be existing folders; a missing one stops the server with an error. Each flag
is given once (`--flag <dir>` or `--flag=<dir>`).
The flags confine the headless server and imply `--headless`; `--connect` with them is an error,
since the server would otherwise drive an app that is not confined.
A path is checked, then opened: another program that swaps a link in between can still win that
race, so the roots keep an agent to the files it was given rather than guarding against other
software on the computer.
## Tools
| Tool | Arguments | What it does |
|---|---|---|
| `list_commands` | `filter?` | Every command with its label and parameter documentation; `filter` matches id and label. |
| `execute` | `command`, `params?` | Run any command (`list_commands`). A failure leaves the model unchanged and names what to fix. |
| `batch` | `commands: [{command, params}]` | Run steps in order, stop at the first error; the reply has every result and the failing step (`failed.index`, `failed.command`, `failed.error`). Steps before it stay applied (`undo` takes them back). |
| `inspect_model` | — | `model.info` (counts, units, definitions), `model.activity` (the step table) and `geometry.bodies` (bodies with measures and persistent face names, which sets name as `{body, face}`). |
| `check_model` | — | `model.check`: errors and warnings, each naming its item. |
| `open` | `path` | By extension: `.modelcraft` (`file.open`), `.solidcraft` (`geometry.open`), `.3mf` (`geometry.import.mesh`). |
| `save` | `path?` | `file.save`: to `path` or the model's own file. |
| `write_deck` | `format` (abaqus, nastran, lsdyna), `path`, `steps?` | `deck.write`. |
| `mesh` | `method`, `params?` | Runs the mesher (`tet`: `mesh.tet.generate`; `planar`: `mesh.planar`; the hex and block meshers as they land) and replies with its result, `model.info` and `mesh.quality`. |
| `solve` | `analysis` (static, frequency, explicit, fluid; heat as it lands), `step?`, `options?` | `solve.<analysis>` with the step and the solver's caps and settings. |
| `show_results` | `step?`, `field?`, `bands?`, `deformed?`, `scale?`, `mode?`, `frame?`, `time?` | The `results.*` commands its arguments ask for; the reply has the contour range (smallest and largest value and their nodes: `field: "displacement"` gives the largest displacement), modes or frames. |
| `probe` | `step?`, `nodes?`, `elements?`, `points?`, `forces?`, `mode?`, `frame?` | `solve.probe`: stored results at nodes, elements (explicit) or points (fluid). |
| `reactions` | `step?`, `sets?`, `about?` | `results.reactions`: reaction force and moment per node set and in total. |
| `screenshot` | `view?`, `source?` (window, model), `width?`, `height?`, `path?` | A PNG image content block and what it shows. Headless: a CPU render framing the model, the geometry and the results display (deformed shape, contour bands). Bridged: the whole app window drawn offscreen (`ui.capture`, the default) or a model render with the app's camera (`ui.render`); `view` turns the app's camera first. |
| `undo`, `redo` | — | `edit.undo`, `edit.redo`. |
Resources: `modelcraft://model` (`model.info`) and `modelcraft://commands` (the command catalog).
Errors: an unknown tool, or arguments that do not match the tool's schema (a missing or unknown
argument, a wrong type, a value outside an enum), are JSON-RPC `-32602` errors; an engine failure
is a tool result with `isError: true` and the engine's message.
## Limits
The server treats its input as hostile. A message line may be at most 16 MiB (the control
channel's limit); a longer one is answered with `request too large` and skipped, and the server
keeps serving. A JSON-RPC batch holds at most 100 messages, a `batch` call at most 10 000
commands; JSON nested deeper than 128 levels is a parse error. Picture sides are clamped to
16–4096 pixels. Every command keeps its own caps (element counts, solver degrees of freedom and
factor size, iterations), and an engine failure or panic leaves the model as it was.
## A typical session
```text
→ {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"me","version":"1"}}}
→ {"jsonrpc":"2.0","method":"notifications/initialized"}
→ {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"open","arguments":{"path":"/work/steps.solidcraft"}}}
→ {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"inspect_model","arguments":{}}}
← … "bodies": [{"name": "Steps", "faces": [{"name": "F1:-x"}, {"name": "F1:+x"}, …]}] …
→ {"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"mesh","arguments":{"method":"tet","params":{"body":"Steps","tolerance":0.5}}}}
→ {"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"batch","arguments":{"commands":[
{"command":"model.units","params":{"system":"mm-t-s-N-MPa"}},
{"command":"model.set.create","params":{"name":"all","geometry":[{"body":"Steps"}]}},
{"command":"model.set.create","params":{"name":"root","geometry":[{"body":"Steps","face":"F1:-x"}]}},
{"command":"model.set.create","params":{"name":"tip","geometry":[{"body":"Steps","face":"F1:+x"}]}},
{"command":"model.material.create","params":{"name":"Steel","elastic":{"type":"isotropic","young":210000,"poisson":0.3},"density":7.85e-9}},
{"command":"model.section.create","params":{"name":"solid","set":"all","material":"Steel","kind":{"type":"solid"}}},
{"command":"model.boundary_condition.create","params":{"name":"clamp","type":"fixed","target":{"set":"root"}}},
{"command":"model.load.create","params":{"name":"push","type":"force","target":{"set":"tip"},"vector":[0,0,-1000]}},
{"command":"model.step.create","params":{"name":"Load","procedure":{"type":"static"},"boundary_conditions":["clamp"],"loads":["push"]}}]}}}
→ {"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"solve","arguments":{"analysis":"static","step":"Load"}}}
→ {"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"show_results","arguments":{"step":"Load","field":"displacement"}}}
← … "contour": {"field": "displacement", "range": {"max": {"node": 25, "value": 0.00485}, …}} …
→ {"jsonrpc":"2.0","id":8,"method":"tools/call","params":{"name":"screenshot","arguments":{"view":"iso"}}}
← {"jsonrpc":"2.0","id":8,"result":{"content":[{"type":"image","mimeType":"image/png","data":"iVBORw0…"},{"type":"text","text":"{\"source\":\"model\",…}"}]}}
```
`crates/mcp/tests/workflow.rs` runs this session on a part of the meshing corpus
(`tests/corpus/stepped-block`) and checks the results only through MCP; `crates/mcp/src/tests.rs`
sends malformed messages, huge parameters, paths outside the roots and unknown tools.