forked from 1b-agent/fieldcraft-upstream
mcp: select and selection tools for the app's viewport selection
Bridged to a running app, select and selection call ui.select and ui.selection; headless they are errors (selection belongs to the window). Closes #73 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
This commit is contained in:
1 parent
f11be61ff3
commit
e6688ef014
4 files changed
+61
-1
No files matched your search
@@ -1,6 +1,6 @@
|
||||
//! Where tool calls end up: an in-process headless session, or a running app's control channel
|
||||
//! (docs/control-protocol.md: `engine.execute`, `engine.commands`, `ui.view`, `ui.camera`,
|
||||
//! `ui.render`, `ui.capture`).
|
||||
//! `ui.render`, `ui.capture`, `ui.select`, `ui.selection`).
|
||||
|
||||
use std::io::{BufRead, BufReader, Read, Write};
|
||||
use std::net::{TcpStream, ToSocketAddrs};
|
||||
@@ -43,6 +43,8 @@ pub trait Backend {
|
||||
fn commands(&mut self) -> Result<Value, String>;
|
||||
/// A PNG of the model, or of the app's window when one is attached.
|
||||
fn picture(&mut self, request: &PictureRequest) -> Result<Picture, String>;
|
||||
/// A request to the app's window (`ui.select`, `ui.selection`): an error without one.
|
||||
fn window(&mut self, method: &str, params: Value) -> Result<Value, String>;
|
||||
/// The folders file access is confined to (checked by the server for the files it writes
|
||||
/// itself).
|
||||
fn roots(&self) -> &Roots;
|
||||
@@ -124,6 +126,10 @@ impl Backend for Headless {
|
||||
Ok(Picture { png, info })
|
||||
}
|
||||
|
||||
fn window(&mut self, method: &str, _params: Value) -> Result<Value, String> {
|
||||
Err(format!("{method} needs the app's window: start the server with --connect to a running FieldCraft"))
|
||||
}
|
||||
|
||||
fn roots(&self) -> &Roots {
|
||||
&self.roots
|
||||
}
|
||||
@@ -267,6 +273,10 @@ impl Backend for Remote {
|
||||
read
|
||||
}
|
||||
|
||||
fn window(&mut self, method: &str, params: Value) -> Result<Value, String> {
|
||||
self.call(method, params)
|
||||
}
|
||||
|
||||
fn roots(&self) -> &Roots {
|
||||
&self.roots
|
||||
}
|
||||
|
||||
@@ -78,6 +78,8 @@ fn initialize_lists_tools_and_resources() {
|
||||
"probe",
|
||||
"reactions",
|
||||
"screenshot",
|
||||
"select",
|
||||
"selection",
|
||||
"undo",
|
||||
"redo",
|
||||
] {
|
||||
@@ -181,6 +183,9 @@ fn hostile_tool_arguments_are_tool_errors() {
|
||||
("screenshot", json!({"camera": {"yaw": 0, "pitch": 0}, "view": "top"})),
|
||||
("batch", json!({"commands": [{"command": "model.info"}, {"params": {}}, {"command": "model.info"}]})),
|
||||
("batch", json!({"commands": vec![json!({"command": "model.info"}); crate::tools::MAX_STEPS + 1]})),
|
||||
("select", json!({"filter": "faces"})),
|
||||
("select", json!({})),
|
||||
("selection", json!({})),
|
||||
("undo", json!({})),
|
||||
("redo", json!({})),
|
||||
] {
|
||||
|
||||
@@ -290,6 +290,41 @@ pub fn tool_definitions() -> Vec<Value> {
|
||||
true,
|
||||
false,
|
||||
),
|
||||
tool(
|
||||
"select",
|
||||
"Select in the viewport",
|
||||
"Change the app window's viewport selection (needs --connect; ui.select): `filter` (faces, edges, vertices, \
|
||||
bodies, nodes, element_faces, elements), `mode` (replace, add, remove, toggle), `clear`, items by name and \
|
||||
label as model.set.create takes them (`geometry`, `nodes`, `elements`, `faces`), a pick at a window point \
|
||||
`at` [x, y] or the visible items in a box `box` [x0, y0, x1, y1]. Returns the selection; its `params` are \
|
||||
a set's members.",
|
||||
obj(
|
||||
json!({
|
||||
"filter": {"type": "string", "enum": ["faces", "edges", "vertices", "bodies", "nodes", "element_faces", "elements"]},
|
||||
"box_mode": {"type": "boolean", "description": "Box selection on or off in the window"},
|
||||
"mode": {"type": "string", "enum": ["replace", "add", "remove", "toggle"]},
|
||||
"clear": {"type": "boolean", "description": "Empty the selection first"},
|
||||
"geometry": {"type": "array", "items": {"type": "object"}, "description": "[{body, face|edge|vertex}]"},
|
||||
"nodes": {"type": "array", "items": {"type": "integer"}},
|
||||
"elements": {"type": "array", "items": {"type": "integer"}},
|
||||
"faces": {"type": "array", "items": {"type": "object"}, "description": "[{element, face}]"},
|
||||
"at": {"type": "array", "items": {"type": "number"}, "description": "Window point [x, y] to pick at"},
|
||||
"box": {"type": "array", "items": {"type": "number"}, "description": "Window box [x0, y0, x1, y1]"},
|
||||
}),
|
||||
&[],
|
||||
),
|
||||
false,
|
||||
false,
|
||||
),
|
||||
tool(
|
||||
"selection",
|
||||
"Read the viewport selection",
|
||||
"The app window's viewport selection (needs --connect; ui.selection): filter, items, `params` (the \
|
||||
selection as a set's members, ready for model.set.create) and what is under the pointer.",
|
||||
obj(json!({}), &[]),
|
||||
true,
|
||||
false,
|
||||
),
|
||||
tool("undo", "Undo", "Take back the last command that changed the model.", obj(json!({}), &[]), false, false),
|
||||
tool("redo", "Redo", "Run the last undone change again.", obj(json!({}), &[]), false, false),
|
||||
]
|
||||
@@ -440,6 +475,14 @@ fn dispatch(b: &mut dyn Backend, name: &str, a: &Map<String, Value>) -> Result<V
|
||||
"show_results" => show_results(b, a),
|
||||
"probe" => b.execute("solve.probe", pick(a, &["step", "nodes", "elements", "points", "forces", "mode", "frame"])),
|
||||
"reactions" => b.execute("results.reactions", pick(a, &["step", "sets", "about"])),
|
||||
"select" => {
|
||||
let p = pick(a, &["filter", "box_mode", "mode", "clear", "geometry", "nodes", "elements", "faces", "at", "box"]);
|
||||
if p.as_object().is_none_or(|o| o.is_empty()) {
|
||||
return Err("select: give `filter`, `mode`, `clear`, items, `at` or `box`".into());
|
||||
}
|
||||
b.window("ui.select", p)
|
||||
}
|
||||
"selection" => b.window("ui.selection", json!({})),
|
||||
"undo" => b.execute("edit.undo", json!({})),
|
||||
"redo" => b.execute("edit.redo", json!({})),
|
||||
other => Err(format!("unknown tool `{}`", other.chars().take(100).collect::<String>())),
|
||||
|
||||
@@ -105,6 +105,8 @@ software on the computer.
|
||||
| `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?`, `camera?`, `source?` (window, model), `width?`, `height?`, `path?` | A PNG image content block and what it shows. Headless: a CPU render framing the model, the geometry, the analysis setup's symbols (boundary conditions, loads, reference points, couplings; `sampled_symbols` lists definitions drawn with a sample) 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 camera to a standard view first; `camera` `{yaw, pitch, distance?}` to any orientation, in degrees (yaw 0 looks along +Y, −45 is the iso view's; pitch is the elevation, clamped to ±90) and model units (`ui.camera`; headless, the picture is framed unless `distance` is given). Give one of the two; the orientation is kept for the next picture, so a sequence of `camera` angles orbits smoothly. |
|
||||
| `select` | `filter?`, `box_mode?`, `mode?`, `clear?`, `geometry?`, `nodes?`, `elements?`, `faces?`, `at?`, `box?` | Bridged only: changes the app's viewport selection (`ui.select`; control-protocol.md, Selection) and returns it. Headless it is an error: selection belongs to the window. |
|
||||
| `selection` | — | Bridged only: the viewport selection (`ui.selection`), with `params`, the selection as a set's members for `model.set.create`. |
|
||||
| `undo`, `redo` | — | `edit.undo`, `edit.redo`. |
|
||||
|
||||
Resources: `fieldcraft://model` (`model.info`) and `fieldcraft://commands` (the command catalog).
|
||||
|
||||
Reference in new issue
Block a user