feat(mcp): support compact single execute replies (#591)
MCP execute accepts verbose: false too, using the same compact reply as batch (#599), and the MCP docs gain a section on compact replies. Co-authored-by: lillith <[email protected]>
This commit is contained in:
1 parent
f0d80a1d7e
commit
8d58e3b9f9
4 files changed
+107
-3
No files matched your search
@@ -61,6 +61,10 @@ PCB / Package, Render, Animation, Simulation, Manufacture/CAM, Drawings.
|
||||
|
||||
## Dimensioning and error reporting
|
||||
|
||||
- MCP `execute` accepts `verbose: false`, sharing the compact reply helper used by
|
||||
`batch`: body changes and created sketch IDs stay, while repeated lists are omitted.
|
||||
Both tools retain full replies by default.
|
||||
|
||||
- Sketch Create groups drawing variants under Line, Rectangle, Circle, Arc, Slot,
|
||||
Polygon and Spline. Each open part remembers its last interactive choice per family
|
||||
across sketch sessions and document-tab switches; closing the part resets these UI choices.
|
||||
|
||||
@@ -540,3 +540,84 @@ fn hidden_body_stays_hidden_after_save_and_reopen() {
|
||||
assert_eq!(base, true);
|
||||
assert_eq!(variant, false, "the hidden body is visible again after reopening");
|
||||
}
|
||||
|
||||
/// Single execute calls share batch compaction without changing geometry or inspection.
|
||||
#[test]
|
||||
fn compact_execute_keeps_changes_ids_and_default_replies() {
|
||||
let mut s = server();
|
||||
let commands = json!([
|
||||
{"command": "solid.box", "params": {"length": 10, "width": 10, "height": 10, "body_name": "Base"}},
|
||||
{"command": "solid.box", "params": {"length": 4, "width": 4, "height": 10, "corner": [3, 3, 5], "operation": "cut", "targets": ["Base"]}},
|
||||
{"command": "sketch.create", "params": {"plane": "XY"}},
|
||||
{"command": "sketch.rectangle.two_point", "params": {"p0": [20, 0], "p1": [30, 10]}}
|
||||
]);
|
||||
let full: Vec<Value> = commands.as_array().unwrap().iter().map(|a| tool(&mut s, "execute", a.clone())).collect();
|
||||
assert_eq!(full[0]["bodies"], json!(["Base"]));
|
||||
assert_eq!(full[3]["curves"], full[3]["created"]["curves"]);
|
||||
let expected_volume = volume(&mut s);
|
||||
for verbose in [true, false] {
|
||||
tool(&mut s, "new_design", json!({}));
|
||||
for (a, expected) in commands.as_array().unwrap().iter().zip(&full) {
|
||||
let mut a = a.clone();
|
||||
a["verbose"] = json!(verbose);
|
||||
let mut actual = tool(&mut s, "execute", a);
|
||||
let mut expected = expected.clone();
|
||||
// Independent executions have different timings; the field must still be present.
|
||||
if expected.get("ms").is_some() {
|
||||
assert!(actual["ms"].is_number());
|
||||
actual.as_object_mut().unwrap().remove("ms");
|
||||
expected.as_object_mut().unwrap().remove("ms");
|
||||
}
|
||||
if !verbose {
|
||||
// Compare against the full result with only the documented repeated fields removed.
|
||||
let o = expected.as_object_mut().unwrap();
|
||||
if o.contains_key("created_bodies") {
|
||||
o.remove("bodies");
|
||||
}
|
||||
for key in ["points", "curves", "constraints", "dimensions"] {
|
||||
if o.get(key).is_some_and(|v| o.get("created").and_then(|c| c.get(key)) == Some(v)) {
|
||||
o.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
assert!((volume(&mut s) - expected_volume).abs() < 1e-9);
|
||||
let inspect = tool(&mut s, "execute", json!({"command": "sketch.inspect", "verbose": false}));
|
||||
assert_eq!(inspect["curves"].as_array().map(Vec::len), Some(4));
|
||||
let invalid = raw_tool(&mut s, "execute", json!({"command": "solid.box", "verbose": "false"}));
|
||||
assert_eq!(invalid["error"]["code"], -32602);
|
||||
assert!((volume(&mut s) - expected_volume).abs() < 1e-9);
|
||||
let error = raw_tool(&mut s, "execute", json!({"command": "not.a.command", "verbose": false}));
|
||||
assert_eq!(error["result"]["isError"], true);
|
||||
}
|
||||
}
|
||||
|
||||
/// A connected app sees exactly one engine call; no inventory inspection is needed.
|
||||
#[test]
|
||||
fn compact_execute_uses_one_backend_call_and_keeps_removed_bodies() {
|
||||
struct ReplyOnly {
|
||||
calls: usize,
|
||||
}
|
||||
impl crate::backend::Backend for ReplyOnly {
|
||||
fn call(&mut self, method: &str, params: Value) -> Result<Value, String> {
|
||||
assert_eq!(method, "engine.execute");
|
||||
assert_eq!(params, json!({"command": "test.feature", "params": {}}));
|
||||
self.calls += 1;
|
||||
Ok(
|
||||
json!({"feature": 7, "name": "Combine1", "bodies": ["Base"], "created_bodies": [], "changed_bodies": ["Base"], "removed_bodies": ["Tool"]}),
|
||||
)
|
||||
}
|
||||
fn has_ui(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn describe(&self) -> String {
|
||||
"test".into()
|
||||
}
|
||||
}
|
||||
let mut b = ReplyOnly { calls: 0 };
|
||||
let r = crate::tools::call_tool(&mut b, "execute", &json!({"command": "test.feature", "verbose": false}));
|
||||
let v: Value = serde_json::from_str(r["content"][0]["text"].as_str().unwrap()).unwrap();
|
||||
assert_eq!(b.calls, 1);
|
||||
assert_eq!(v, json!({"feature": 7, "name": "Combine1", "created_bodies": [], "changed_bodies": ["Base"], "removed_bodies": ["Tool"]}));
|
||||
}
|
||||
@@ -68,6 +68,7 @@ pub fn tool_definitions() -> Vec<Value> {
|
||||
json!({
|
||||
"command": string("Command id (see list_commands)"),
|
||||
"params": {"type": "object", "description": "The command's parameters"},
|
||||
"verbose": {"type": "boolean", "description": "false: omit the full feature body list and sketch ids repeated outside `created`; keep created_bodies, changed_bodies and removed_bodies (default true)"},
|
||||
}),
|
||||
&["command"],
|
||||
),
|
||||
@@ -344,7 +345,8 @@ fn dispatch(b: &mut dyn Backend, name: &str, a: &Map<String, Value>) -> Result<V
|
||||
}
|
||||
"execute" => {
|
||||
let id = str_arg(a, "command").ok_or("`command` is required")?;
|
||||
exec(b, id, a.get("params").cloned().filter(Value::is_object).unwrap_or(json!({})))
|
||||
let v = exec(b, id, a.get("params").cloned().filter(Value::is_object).unwrap_or(json!({})))?;
|
||||
Ok(if a.get("verbose").and_then(Value::as_bool).unwrap_or(true) { v } else { compact(v) })
|
||||
}
|
||||
"inspect_design" => {
|
||||
let measure = a.get("measure").and_then(Value::as_bool).unwrap_or(true);
|
||||
@@ -484,7 +486,7 @@ fn batch(b: &mut dyn Backend, a: &Map<String, Value>) -> Value {
|
||||
text_result(&json!({"ok": true, "completed": results.len(), "results": results}), false)
|
||||
}
|
||||
|
||||
/// A step's result without what the design already had (`batch` with `verbose: false`): a
|
||||
/// A command's result without what the design already had (`execute` or `batch` with `verbose: false`): a
|
||||
/// feature's full body list (its created, changed and removed bodies stay), and the sketch ids
|
||||
/// a sketch command repeats outside `created`.
|
||||
fn compact(mut v: Value) -> Value {
|
||||
|
||||
+18
-1
@@ -50,7 +50,7 @@ Claude Desktop (`claude_desktop_config.json`), bridged to a running app:
|
||||
| Tool | Arguments | What it does |
|
||||
|---|---|---|
|
||||
| `list_commands` | `filter?` | The command catalog: id, label, tab/panel, shortcut, parameter docs, enabled now. `filter` matches id, label, tab and panel. |
|
||||
| `execute` | `command`, `params?` | Run any command with JSON parameters (`list_commands` shows them). Failures leave the design unchanged. A feature command replies with the feature (`feature`, `name`), every body (`bodies`) and the bodies it created, changed and removed (`created_bodies`, `changed_bodies`, `removed_bodies`). |
|
||||
| `execute` | `command`, `params?`, `verbose?` (default true) | Run any command with JSON parameters (`list_commands` shows them). Failures leave the design unchanged. A feature command replies with the feature (`feature`, `name`), every body (`bodies`) and the bodies it created, changed and removed (`created_bodies`, `changed_bodies`, `removed_bodies`). `verbose: false` uses the same compact reply as `batch`. |
|
||||
| `batch` | `commands: [{command, params}]`, `rollback?` (default true), `verbose?` (default true) | Run steps in order, stop at the first error and undo the completed steps. The reply names the failing step (`failed.index`, `failed.command`, `failed.error`) and every result before it. `verbose: false` keeps the reply small in large designs: feature steps leave out the full body list (`created_bodies`, `changed_bodies`, `removed_bodies` stay) and sketch steps the ids they repeat outside `created`. |
|
||||
| `inspect_design` | `measure?` (default true), `sketches?` (default true) | Parameters, timeline (type, suppressed, rolled back, error, warning, timing), bodies (volume, area, centre of mass, bbox, face/edge/vertex counts, visible), sketches (curves, points, constraints, profiles, DOF, solve status, visible), selection, undo depth. Hide helper sketches, bodies or construction planes with `execute` `browser.visibility` (`{items, visible}` or `{folder, visible}`). |
|
||||
| `measure` | `body?` | Volume (mm³), area (mm²), centre of mass, bbox and topology counts per body, with totals. |
|
||||
@@ -82,6 +82,23 @@ however long the history is. Batches that call `file.new` or `doc.open` get the
|
||||
not its file path. (An older app without checkpoints falls back to undoing as many steps as
|
||||
the batch added to the undo history.)
|
||||
|
||||
## Compact replies
|
||||
|
||||
`execute` and `batch` default to `verbose: true`, preserving the full command results.
|
||||
For long modelling sessions, set `verbose: false` at the tool-argument level:
|
||||
|
||||
```json
|
||||
{"name": "execute", "arguments": {"command": "solid.box", "params": {"length": 10, "width": 10, "height": 10}, "verbose": false}}
|
||||
```
|
||||
|
||||
Feature replies omit the full `bodies` list while keeping `feature`, `name`,
|
||||
`created_bodies`, `changed_bodies` and `removed_bodies`. A cut can therefore report a
|
||||
changed body even when no body name was added or removed. Sketch replies keep new
|
||||
IDs under `created` and omit only identical top-level `points`, `curves`,
|
||||
`constraints` and `dimensions` lists. Other fields and errors are unchanged;
|
||||
inspection commands still return their complete results. Compaction uses the
|
||||
engine's reply directly, without an additional design-inspection request.
|
||||
|
||||
## Demo: an enclosure built over MCP
|
||||
|
||||
`examples/demo/enclosure_mcp.py` is a small MCP client. It drives a running app (`solidcraft
|
||||
|
||||
Reference in new issue
Block a user