MCP: leave inspect_design and tool JSON as on main (moved to their own PR)

Per review, this PR keeps to long requests, honest batch outcomes and tool
names in a batch. inspect_design is back to main's arguments and output
(full sketches by default, no bodies filter), document.inspect's sketch
entries are unchanged, and tool results are pretty-printed JSON again; the
compact inspect_design moves to its own PR, off by default. The docs no
longer describe agent-cursor batch behaviour that is not in this branch.

An unknown argument's -32602 error still lists the tool's arguments: same
code and same leading text, only more helpful.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
SS-EN
2026-10-19 11:59:49 -04:00
parent 101272ad42
commit df488f9a52
6 changed files with 29 additions and 171 deletions
+1 -5
View File
@@ -256,11 +256,7 @@ fn inspect(s: &mut Session, p: &Value) -> Result<Value> {
let sketches: Vec<Value> = st
.sketches
.iter()
.map(|ss| {
json!({"id": ss.feature, "name": ss.name, "dof": ss.report.dof, "status": ss.report.status, "plane": ss.plane,
"points": ss.sketch.points.len(), "curves": ss.sketch.curves.len(), "constraints": ss.sketch.constraints.len(),
"profiles": ss.profiles.len(), "visible": crate::view::sketch_shown(s, ss.feature)})
})
.map(|ss| json!({"id": ss.feature, "name": ss.name, "dof": ss.report.dof, "status": ss.report.status, "curves": ss.sketch.curves.len(), "profiles": ss.profiles.len(), "visible": crate::view::sketch_shown(s, ss.feature)}))
.collect();
Ok(json!({
"name": s.doc.name, "units": s.doc.units, "metadata": s.doc.metadata, "path": s.path, "dirty": s.is_dirty(), "revision": s.revision,
+2 -54
View File
@@ -167,7 +167,7 @@ fn agent_builds_box_fillet_hole() {
assert!((8..=9).contains(&m["bodies"][0]["faces"].as_u64().unwrap()), "{m}");
assert!(rel(m["bodies"][0]["bbox"]["max"][0].as_f64().unwrap(), 40.0) < 1e-6, "{m}");
let d = tool(&mut s, "inspect_design", json!({"sketches": true}));
let d = tool(&mut s, "inspect_design", json!({}));
assert_eq!(d["timeline"].as_array().unwrap().len(), 5, "{d}");
assert!(d["timeline"].as_array().unwrap().iter().all(|f| f["error"].is_null()), "{d}");
assert_eq!(d["sketches"][0]["curves"].as_array().unwrap().len(), 4, "{d}");
@@ -1812,65 +1812,13 @@ fn batch_steps_may_name_tools() {
assert!(volume(&mut s) > 4000.0);
}
/// `inspect_design` is small by default (#15): sketch summaries, compact JSON. Full sketch
/// contents only for the sketches asked for; `bodies` keeps only those bodies.
#[test]
fn inspect_design_summarises_sketches_unless_asked() {
let mut s = server();
let points: Vec<[f64; 2]> = (0..720)
.map(|i| {
let t = f64::from(i) / 720.0 * std::f64::consts::TAU;
[30.0 * t.cos(), 30.0 * t.sin()]
})
.collect();
tool(
&mut s,
"batch",
json!({"commands": [
{"command": "sketch.create", "params": {"plane": "XY"}},
{"command": "sketch.line", "params": {"points": points, "closed": true}},
{"command": "sketch.finish"},
{"command": "solid.extrude", "params": {"distance": 5}},
]}),
);
let text = |s: &mut Server, args: Value| {
let r = raw_tool(s, "inspect_design", args);
(r["result"]["isError"] == json!(true), r["result"]["content"][0]["text"].as_str().unwrap_or_default().to_string())
};
let (_, summary) = text(&mut s, json!({"measure": false}));
assert!(summary.len() < 4000, "{} bytes: {summary}", summary.len());
assert!(!summary.contains("\n "), "compact JSON");
let d: Value = serde_json::from_str(&summary).unwrap();
let sk = &d["sketches"][0];
assert!(sk["points"].as_u64().is_some_and(|n| n >= 720), "{sk}");
assert_eq!((&sk["curves"], &sk["profiles"]), (&json!(720), &json!(1)), "{sk}");
assert!(sk["constraints"].is_u64() && sk["dof"].is_number() && sk["plane"].is_object(), "{sk}");
assert_eq!(text(&mut s, json!({"measure": false, "sketches": false})).1, summary);
assert_eq!(text(&mut s, json!({"measure": false, "sketches": "summary"})).1, summary);
let name = sk["name"].as_str().unwrap().to_string();
let (_, full) = text(&mut s, json!({"measure": false, "sketches": [name]}));
assert!(full.len() > 50_000 && full.len() > 20 * summary.len(), "{} vs {}", full.len(), summary.len());
let f: Value = serde_json::from_str(&full).unwrap();
assert_eq!(f["sketches"][0]["curves"].as_array().map(Vec::len), Some(720));
assert_eq!(text(&mut s, json!({"measure": false, "sketches": true})).1.len(), full.len());
let (err, e) = text(&mut s, json!({"sketches": ["Nope"]}));
assert!(err && e.contains("no sketch") && e.contains(&name), "{e}");
let (_, one) = text(&mut s, json!({"bodies": ["Body1"]}));
let one: Value = serde_json::from_str(&one).unwrap();
assert_eq!(one["bodies"].as_array().map(Vec::len), Some(1), "{one}");
assert!(one["bodies"][0]["volume_mm3"].as_f64().is_some_and(|v| v > 0.0) && one["bodies"][0]["visible"] == true, "{one}");
let (err, e) = text(&mut s, json!({"bodies": ["Nope"]}));
assert!(err && e.contains("no body `Nope`") && e.contains("Body1"), "{e}");
}
/// An unknown argument is refused with the tool's real arguments listed.
#[test]
fn unknown_arguments_list_the_real_ones() {
let mut s = server();
for (tool_name, want) in [
("screenshot", &["view", "camera", "path", "width", "height", "source", "grid", "axes"][..]),
("inspect_design", &["measure", "sketches", "bodies"][..]),
("inspect_design", &["measure", "sketches"][..]),
("body_topology", &["body"][..]),
] {
let args = if tool_name == "body_topology" { json!({"body": "Body1", "nonsense": 3}) } else { json!({"nonsense": 3}) };
+20 -105
View File
@@ -129,19 +129,12 @@ pub fn tool_definitions() -> Vec<Value> {
"Inspect the design",
"The whole design: user parameters (expression, value, errors), the timeline (features with type, suppressed, \
rolled back, error, warning, timing), bodies (volume, area, centre of mass, bounding box, face/edge/vertex \
counts), sketches, the active sketch, selection and undo depth. Sketches are summarised by default (id, name, \
plane, counts of points, curves, constraints and profiles, degrees of freedom, solve status); their full contents \
(curves with ids and coordinates, points, constraints, profiles) only for the sketches named in `sketches`, or \
all with `sketches: true` (large: a 720-point sketch is some 200 KB). Arguments: measure, sketches, bodies.",
counts), sketches (curves with ids and coordinates, points, constraints, profiles, degrees of freedom, solve \
status), the active sketch, selection and undo depth.",
obj(
json!({
"measure": {"type": "boolean", "description": "Measure bodies (default true)"},
"sketches": {
"type": ["boolean", "string", "array"],
"items": {"type": ["string", "integer"]},
"description": "Which sketches in full: a list of sketch names or ids, true (all of them), or false / \"summary\" (none: summaries only, the default)",
},
"bodies": {"type": "array", "items": {"type": "string"}, "description": "Only these bodies, by name (default: all)"},
"sketches": {"type": "boolean", "description": "Full sketch contents (default true; false = summary only)"},
}),
&[],
),
@@ -169,8 +162,7 @@ pub fn tool_definitions() -> Vec<Value> {
"List edges and faces",
"The edges (index, midpoint, length, endpoints) and faces (index, name, area, centroid, plane normal, a `point` that lies \
on the face, and for a cylindrical face its `cylinder`: origin, axis, radius) of a body. Use an edge midpoint as the \
[x,y,z] point that picks the edge for fillet and chamfer; a face's point or its name picks the face for the face commands. \
Arguments: body (required).",
[x,y,z] point that picks the edge for fillet and chamfer; a face's point or its name picks the face for the face commands.",
obj(json!({"body": string("Body name, e.g. \"Body1\"")}), &["body"]),
Effect::ReadOnly,
),
@@ -201,8 +193,7 @@ pub fn tool_definitions() -> Vec<Value> {
Connected to the app: a screenshot of the window (source \"window\", the default) or a render of the model \
with the app's camera (source \"model\"). Both show an active section (inspect.section) with its cut face \
capped. `view` first turns the camera to a standard view or a saved named view (view.save), `camera` to any camera (as view.apply). \
`grid: false` and `axes: false` leave the ground grid (on Z 0) and the origin's axes out of a model render. \
Arguments: view, camera, path, width, height, source, grid, axes (all optional).",
`grid: false` and `axes: false` leave the ground grid (on Z 0) and the origin's axes out of a model render.",
obj(
json!({
"view": string(
@@ -362,12 +353,11 @@ pub(crate) fn check_arguments(name: &str, args: &Value) -> Result<(), String> {
Ok(())
}
/// A tool result: content blocks plus the error flag. JSON is compact (not pretty-printed): the
/// agent reads every byte.
/// A tool result: content blocks plus the error flag.
fn text_result(v: &Value, is_error: bool) -> Value {
let text = match v {
Value::String(s) => s.clone(),
v => v.to_string(),
v => serde_json::to_string_pretty(v).unwrap_or_default(),
};
let mut r = json!({"content": [{"type": "text", "text": text}]});
if is_error {
@@ -415,7 +405,19 @@ fn dispatch(b: &mut dyn Backend, name: &str, a: &Map<String, Value>) -> Result<V
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" => inspect_design(b, a),
"inspect_design" => {
let measure = a.get("measure").and_then(Value::as_bool).unwrap_or(true);
let mut d = b.call("document.inspect", json!({"measure": measure}))?;
if a.get("sketches").and_then(Value::as_bool).unwrap_or(true) {
let ids: Vec<Value> = d["sketches"].as_array().map(|v| v.iter().map(|s| s["id"].clone()).collect()).unwrap_or_default();
let full: Vec<Value> = ids
.into_iter()
.map(|id| exec(b, "sketch.inspect", json!({"sketch": id.clone()})).unwrap_or_else(|e| json!({"id": id, "error": e})))
.collect();
d["sketches"] = Value::Array(full);
}
Ok(d)
}
"pending" => pending(b),
"measure" => {
if let Some(items) = a.get("items") {
@@ -461,93 +463,6 @@ fn filter_commands(all: Value, filter: Option<&str>) -> Value {
Value::Array(all.as_array().map(|v| v.iter().filter(|c| hit(c)).cloned().collect()).unwrap_or_default())
}
/// Names for an error message: at most 40 of them.
fn name_list(names: &[String]) -> String {
let mut out = names.iter().take(40).cloned().collect::<Vec<_>>().join(", ");
if names.len() > 40 {
out.push_str(&format!(", … ({} in all)", names.len()));
}
if out.is_empty() { "none".into() } else { out }
}
/// `inspect_design`: `document.inspect`, with sketch summaries unless sketches are asked for in
/// full (by name or id, or all), and only the bodies asked for.
fn inspect_design(b: &mut dyn Backend, a: &Map<String, Value>) -> Result<Value, String> {
let measure = a.get("measure").and_then(Value::as_bool).unwrap_or(true);
let bodies: Option<Vec<String>> = match a.get("bodies").filter(|v| !v.is_null()) {
None => None,
Some(v) => Some(
v.as_array()
.and_then(|l| l.iter().map(|n| n.as_str().map(str::to_string)).collect::<Option<Vec<_>>>())
.ok_or("inspect_design: `bodies` must be a list of body names")?,
),
};
// With `bodies`, only those are measured.
let mut d = b.call("document.inspect", json!({"measure": measure && bodies.is_none()}))?;
if let Some(want) = bodies {
let all: Vec<Value> = d["bodies"].as_array().cloned().unwrap_or_default();
let names: Vec<String> = all.iter().filter_map(|x| x["name"].as_str().map(str::to_string)).collect();
if let Some(missing) = want.iter().find(|n| !names.contains(n)) {
return Err(format!("inspect_design: no body `{missing}` (bodies: {})", name_list(&names)));
}
let mut kept: Vec<Value> = all.into_iter().filter(|x| x["name"].as_str().is_some_and(|n| want.iter().any(|w| w == n))).collect();
if measure && !want.is_empty() {
let m = exec(b, "inspect.measure", json!({"bodies": want}))?;
for body in &mut kept {
let measured = m["bodies"].as_array().and_then(|l| l.iter().find(|x| x["name"] == body["name"])).cloned();
if let (Some(Value::Object(fields)), Some(o)) = (measured, body.as_object_mut()) {
for (k, v) in fields {
o.entry(k).or_insert(v);
}
}
}
}
d["bodies"] = Value::Array(kept);
}
let sketches: Vec<Value> = d["sketches"].as_array().cloned().unwrap_or_default();
let full: Vec<Value> = match a.get("sketches") {
None | Some(Value::Null) | Some(Value::Bool(false)) => Vec::new(),
Some(Value::String(t)) if t == "summary" => Vec::new(),
Some(Value::Bool(true)) => sketches.iter().map(|x| x["id"].clone()).collect(),
Some(Value::String(t)) if t == "full" || t == "all" => sketches.iter().map(|x| x["id"].clone()).collect(),
Some(Value::Array(list)) => {
let mut ids = Vec::new();
for want in list {
let found = sketches.iter().find(|x| match want {
Value::String(n) => x["name"].as_str() == Some(n.as_str()) || x["id"].as_u64().is_some_and(|id| n.parse::<u64>() == Ok(id)),
Value::Number(_) => x["id"] == *want,
_ => false,
});
match found {
Some(x) => ids.push(x["id"].clone()),
None => {
let names: Vec<String> = sketches.iter().filter_map(|x| x["name"].as_str().map(str::to_string)).collect();
return Err(format!("inspect_design: no sketch {want} (sketches: {})", name_list(&names)));
}
}
}
ids
}
Some(other) => {
return Err(format!("inspect_design: `sketches` must be true, false, \"summary\" or a list of sketch names (got {other})"));
}
};
if !full.is_empty() {
let out: Vec<Value> = sketches
.into_iter()
.map(|x| {
if full.contains(&x["id"]) {
exec(b, "sketch.inspect", json!({"sketch": x["id"].clone()})).unwrap_or_else(|e| json!({"id": x["id"], "error": e}))
} else {
x
}
})
.collect();
d["sketches"] = Value::Array(out);
}
Ok(d)
}
/// `pending`: the end of a request still running when its call returned (bridged only; a
/// headless session runs every request to its end).
fn pending(b: &mut dyn Backend) -> Result<Value, String> {
+2 -2
View File
@@ -78,7 +78,7 @@ MCP server sends its folders this way on every request ([mcp.md](mcp.md)). Witho
|---|---|---|
| `engine.execute` | `command`: command id, `params`: JSON object | the command's result |
| `engine.script` | `commands`: `[{command, params}, …]` | results, stops at the first error |
| `engine.batch` | `commands`: `[{command, params}, …]` (at most 10 000), `rollback?`: bool (default true), `timings?`, `profile?`: bool (default false) | `{completed, results, undone}`, plus `failed: {index, command, error}` when a step fails, and with `timings` `timings: [{index, command, ms}]` (the failing step included) and `total_ms`; `profile` adds each step's `profile: {boolean_ms, tessellate_ms, other_ms}`. Runs the steps in one request, so within one frame, each as `engine.execute` runs it. The first failure stops the batch, and with `rollback` the design goes back to how it was before it (`undone`: the steps taken back; `rolled_back`: whether the design really is back at its checkpoint). Refused before anything runs, with an error starting `engine.batch unavailable`, when a step loads a file (`doc.open`, `file.insert_step`, `file.insert_mesh`). Send such steps one by one. While the agent cursor is shown, the pointer first acts out a short summary (the toolbar buttons of a few of its commands, within 1.5 s), then the whole batch runs. Answered when done, for up to 3600 s |
| `engine.batch` | `commands`: `[{command, params}, …]` (at most 10 000), `rollback?`: bool (default true), `timings?`, `profile?`: bool (default false) | `{completed, results, undone}`, plus `failed: {index, command, error}` when a step fails, and with `timings` `timings: [{index, command, ms}]` (the failing step included) and `total_ms`; `profile` adds each step's `profile: {boolean_ms, tessellate_ms, other_ms}`. Runs the steps in one request, so within one frame, each as `engine.execute` runs it. The first failure stops the batch, and with `rollback` the design goes back to how it was before it (`undone`: the steps taken back; `rolled_back`: whether the design really is back at its checkpoint). Refused before anything runs, with an error starting `engine.batch unavailable`, while the agent cursor is shown or when a step loads a file (`doc.open`, `file.insert_step`, `file.insert_mesh`). Send such steps one by one. Answered when done, for up to 3600 s |
| `engine.commands` | — | every command: id, label, tab, panel, icon, shortcut, params doc, enabled |
| `document.inspect` | `measure?`: bool | parameters, timeline (errors, timings), bodies, sketches, selection |
@@ -160,7 +160,7 @@ labels; `prefs:measure_axis_distances` identifies the checkbox.
| `ui.prefs` | any of `default_units`, `nav` (`fusion`, `solidworks`, `inventor`, `alias`, `blender`, `tinkercad`, `openscad`, `touchpad`), `zoom_reverse`, `pan_reverse_x` (default false: horizontal scrolling pans the other way), `zoom_to_cursor`, `smooth_wheel_zoom` (default true), `trackpad_pan` (default true: two-finger scroll pans, except Touchpad always zooms; pinch zooms), `orbit_around` (`view_centre` (default: what Pan moves and Fit frames), `origin` (the world origin stays still on screen), `cursor` (the model point under the pointer when the drag began); the older `orbit_cursor` boolean still sets `cursor` when true), `look_at_anim`, `look_at_speed`, `middle_double_click` (`home` (default), `fit`, `off`), `msaa`, `show_frame_rate` (default false: frames per second in the view's bottom-right corner; the view redraws every frame while it is on), `length_decimals`, `angle_decimals`, `theme` (`dark`, `light`, `system` or a theme file's id: a built-in one such as `gruvbox-dark`, or an imported one; the reply's `theme_ids` lists them), `theme_import` (a `solidcraft-theme/1` file's JSON or text: adds the theme and turns it on; the reply's `theme_imported` is its id), `theme_remove` (an imported theme's id), `theme_export: true` (the reply's `theme_file` is the theme on now as a complete file; see [colour themes](book/src/guide/ui-tour.md#colour-themes)), `language` (`en`, `ja`, `de`, `fr`, `ko`, `ms`, `ru`, `uk`, `zh`; see [localization](localization.md)), `ui_scale`, `ui_font` (an installed font family or a font file for the interface; empty for the built-in font), `up_axis` (`z`, `y`), `offer_stand_up`, `color_vision` (`off` (default), `protan`, `deutan`, `tritan`: colour vision modes, #778), `check_updates` (default true; [updates](book/src/guide/updates.md)); `open` (a page: General, Navigation, Keyboard Shortcuts, Design, Graphics, Sketch, Selection, AI) opens the Preferences dialog there; `button`: `ok`, `cancel`, `apply`, `defaults`; `stand_up: true` stands up a just-opened Y-up file, `stand_up: false` leaves it as it is ("Leave it": the offer closes and `offer_stand_up` stays on); the offer's buttons are the handles `stand_up:stand`, `stand_up:leave` and `stand_up:never` | sets preferences (kept between runs), returns them all plus `dialog` (the open page or null), `dark` and `stand_up_offer` |
| `ui.capture` | `action?`: `capture`, `revert`, `cancel` | answers the Capture Position question (asked when a command starts or the design is saved while components were moved without capturing); without `action`, what waits |
| `ui.window` | `action`: minimize, maximize, restore, toggle, close | what the title bar's caption buttons do (on Windows and Linux the application bar is the title bar); close asks about unsaved designs first |
| `ui.agent_cursor` | `show` (bool), `speed`: a number from 1 to 5 (times the normal speed) or `"instant"` (no animation: the command runs at once and the pointer jumps to where it acted), `follow_camera` (bool); all optional | the settings. While the cursor is shown, `engine.execute` first glides to the command's toolbar button and to its picks (at most three: a long point list is summarised by its first, middle and last point on screen), then runs and replies, within 1.5 s per request whatever its size (the glides get shorter); an `engine.batch` is acted out as one request. Later requests wait their turn. `"animate": false` on a call skips it |
| `ui.agent_cursor` | `show` (bool), `speed`: a number from 1 to 5 (times the normal speed) or `"instant"` (no animation: the command runs at once and the pointer jumps to where it acted), `follow_camera` (bool); all optional | the settings. While the cursor is shown, `engine.execute` first glides to the command's toolbar button and to each pick, then runs and replies; later requests wait their turn. `"animate": false` on a call skips it |
| `ui.agent_chat` | `features?`, `toolbar_button?`, `permissions?` (`ask`, `edits`, `all`), `add_agent?` ({`name`, `kind`: `acp` or `api`, `command` or `base_url` + `model`}; no keys), `default_agent?`, `agent?` (the conversation's agent, by id), `mode?` (the agent's own mode), `model?` (one of its models), `open?`, `side?` (`left`, `right`), `minimized?`, `popped?` (in its own window), `width?`, `picker?`, `refresh?`, `connect?`, `stop?`, `attach?`: a file's path (a picture, a text file or a CAD file, for the next message; CAD files are imported at once), `remove_file?` (index of a file on the next message), `send?`: text (files alone make a message too), `attach_view?`, `permission?`: an option id or `"cancel"` (the newest question of the agent's own), `approve?`: true, false or `"always"` (the oldest request held for Allow/Deny), `cancel?` (Stop: the queue stays), `send_now?` (stop the running step, then send the queued messages), `remove_pending?` (index), `new_chat?` (clear the chat, new session), `undo?` (the last message's changes) | the chat's state: `features`, `agents` (found and added), `currentAgent`, `state` (off, starting, ready, busy, auth, exited), `popped`, `window` (its place and size), `agent`, `modes`, `mode`, `models`, `model`, `authMethods`, `toolsPort`, `pid`, `waiting`, `turn`, `pending` (queued messages, with their `files`), `attachments` (the files on the next message, by name), `loading` (pictures still being read for it, by name), `stopped`, `detection` and the conversation (`items`). With `features` off only the settings apply. Desktop only; see [Agent chat](agent-chat.md) |
| `ui.resize` | `width`, `height` | |
| `app.quit` | `force?` (quit without asking about unsaved designs) | `quitting` (the window closes), `asking` (a "save changes?" prompt is open) |
+3 -4
View File
@@ -144,8 +144,8 @@ They cover the workflow, the command shapes, how to verify with `measure`, and t
|---|---|---|
| `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?`, `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), `timings?`, `profile?` (default false) | 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`), every result before it, `rolled_back` (true only when the design really went back to where the batch started), `undone`, `applied` (the indices of the steps whose changes are in the design) and `not_run` (the steps never sent). A step may name a tool instead of a command, with that tool's arguments as `params`: `set_parameter` (runs `parameters.change`; `value` or `expression`), `list_commands` (`engine.commands`, filtered by `filter` as the tool filters it), `execute` (`{command, params}`), `measure`, `inspect_design` (`document.inspect`, summaries), `undo`, `redo`, `export`, `save`, `open`, `new_design` and `crash_reports`. `screenshot`, `body_topology` (use `model.edges` and `model.faces`), `batch` and `pending` can't be steps: such a batch fails before anything is sent. `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`. Bridged to an app that has `engine.batch`, the whole batch is one control request (one frame of the app), also while the app shows the agent cursor (the pointer acts out a short summary of the batch first). Older apps get the steps one by one. `timings: true` adds `timings: [{index, command, ms}]` for every step run, the failing one included, and `total_ms`. `profile: true` also adds each step's `profile: {boolean_ms, tessellate_ms, other_ms}`, its time in kernel booleans, in tessellation and the rest. Steps are timed where they run: in the app when bridged. An app without `engine.batch` is timed step by step, without `profile`. |
| `inspect_design` | `measure?` (default true), `sketches?` (a list of sketch names or ids, `true` for all, or `false` / `"summary"`: the default), `bodies?` (names; default all) | Design properties (`metadata`: project, author… and other tools' keys; set them with `execute` `document.metadata.set`), parameters, timeline (type, suppressed, rolled back, error, warning, timing), bodies (volume, area, centre of mass, bbox, face/edge/vertex counts, visible; `removed_later: true` while a sketch is being edited on a rolled-back timeline and a later feature deletes the body, which is then not shown), sketches, selection, undo depth. Each sketch is summarised (`id`, `name`, `plane`, the number of `points`, `curves`, `constraints` and `profiles`, `dof`, `status`, `visible`); its full contents (curves with ids and coordinates, points, constraints, profiles), as `execute` `sketch.inspect` gives them, only when `sketches` names it (or is `true`): a sketch of 720 points is some 200 KB in full. `bodies` keeps only the bodies named (and measures only them); an unknown sketch or body name is an error that lists the names there are. Hide helper sketches, bodies or construction planes with `execute` `browser.visibility` (`{items, visible}` or `{folder, visible}`), and joint glyphs with `{joints: [id or name], visible}` or `{folder: "joints", visible}` (a hidden joint still constrains and drives; `joint.list` reports each joint's `visible`). |
| `batch` | `commands: [{command, params}]`, `rollback?` (default true), `verbose?` (default true), `timings?`, `profile?` (default false) | 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`), every result before it, `rolled_back` (true only when the design really went back to where the batch started), `undone`, `applied` (the indices of the steps whose changes are in the design) and `not_run` (the steps never sent). A step may name a tool instead of a command, with that tool's arguments as `params`: `set_parameter` (runs `parameters.change`; `value` or `expression`), `list_commands` (`engine.commands`, filtered by `filter` as the tool filters it), `execute` (`{command, params}`), `measure`, `inspect_design` (`document.inspect`: each sketch as its counts, not its full contents), `undo`, `redo`, `export`, `save`, `open`, `new_design` and `crash_reports`. `screenshot`, `body_topology` (use `model.edges` and `model.faces`), `batch` and `pending` can't be steps: such a batch fails before anything is sent. `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`. Bridged to an app that has `engine.batch`, the whole batch is one control request (one frame of the app). Older apps, and apps showing the agent cursor, get the steps one by one. `timings: true` adds `timings: [{index, command, ms}]` for every step run, the failing one included, and `total_ms`. `profile: true` also adds each step's `profile: {boolean_ms, tessellate_ms, other_ms}`, its time in kernel booleans, in tessellation and the rest. Steps are timed where they run: in the app when bridged. An app without `engine.batch` is timed step by step, without `profile`. |
| `inspect_design` | `measure?` (default true), `sketches?` (default true) | Design properties (`metadata`: project, author… and other tools' keys; set them with `execute` `document.metadata.set`), parameters, timeline (type, suppressed, rolled back, error, warning, timing), bodies (volume, area, centre of mass, bbox, face/edge/vertex counts, visible; `removed_later: true` while a sketch is being edited on a rolled-back timeline and a later feature deletes the body, which is then not shown), 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}`), and joint glyphs with `{joints: [id or name], visible}` or `{folder: "joints", visible}` (a hidden joint still constrains and drives; `joint.list` reports each joint's `visible`). |
| `measure` | `body?` or `items?` | Volume (mm³), area (mm²), centre of mass, bbox and topology counts per body, with totals. With `items` (one or two selections as `execute` takes them: `face`, `edge`, `vertex`, `sketch_curve`, `profile`), the picked geometry instead: see below. |
| `body_topology` | `body` | Edges (index, midpoint, length, ends) and faces (index, name, area, centroid, normal, `point`, and for a cylindrical face its `cylinder`: `origin`, `axis`, `radius`). An edge midpoint is the `[x,y,z]` that picks that edge for fillet and chamfer. A face's `point` lies on it and picks it (its centroid may not: that of a face with a hole is in the hole); a face's `name` picks it for the commands that take faces (`face.delete`, `solid.shell`, `solid.draft`, `solid.offset_face`, `solid.split_face`, `solid.replace_face`, and `face` of `sheet.convert`). |
| `set_parameter` | `name`, `value` or `expression` (number or expression), `unit?`, `comment?` | Create or change a user parameter and recompute. `expression` is another name for `value` (the one `parameters.change` uses): give one of them, or both only when they agree; neither, or two that differ, is an error. Reports `recomputed` (features rebuilt), `restored` (features whose earlier result for the same inputs was reused, e.g. a value set back) and features that now fail. |
@@ -187,8 +187,7 @@ Resources: `solidcraft://design` (inspect JSON with measurements) and `solidcraf
Errors: an unknown tool or arguments that don't match the tool's schema are JSON-RPC
`-32602` errors (an unknown argument's error lists the tool's arguments); an engine failure
(bad command parameters, a feature that fails) is a tool result with `isError: true` and the
engine's message, which the model can read and correct. Tool results that are JSON are compact
(not pretty-printed).
engine's message, which the model can read and correct.
### Long-running requests
+1 -1
View File
@@ -133,7 +133,7 @@ After `set_parameter L=120` the volume is 35335.2 mm³, and the holes stay in th
|---|---|
| `list_commands {}` | **Over 100 KB.** Never call it unfiltered; filter on a distinctive word such as `"hole"` or `"slot"`. |
| `body_topology` | 25-45 KB |
| `inspect_design {}` | 12 KB; sketches are summaries (counts, DOF, plane) unless named in `sketches` |
| `inspect_design {}` | 12 KB, and it grows with the sketches |
| `inspect_design {"measure":false,"sketches":false}` | 4 KB: parameters, timeline errors, bodies |
| `sketch.inspect` | 5 KB per small sketch |
| `parameters.list {"filter":"feature"}` | 3 KB |