mcp, cli: list each command's parameters as JSON Schema

CommandSpec::listing gives id, label, the parameter doc and the schema; MCP
list_commands, the control channel's engine.commands and the new
fieldcraft-cli commands --json use it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-14 13:20:04 -04:00
1 parent 217628ead4
commit 188f2d5fc4
7 files changed
+25 -6

No files matched your search

+13
View File
@@ -121,6 +121,19 @@ pub fn commands() -> &'static [CommandSpec] {
cmd::ALL
}
impl CommandSpec {
/// The command as JSON for listings (`fieldcraft-cli commands --json`, the control channel's
/// `engine.commands`, MCP `list_commands`): id, label, the parameter doc and, when described,
/// the parameters as JSON Schema.
pub fn listing(&self) -> Value {
let mut v = serde_json::json!({"id": self.id, "label": self.label, "params": self.params});
if let Some(form) = self.form {
v["schema"] = schema::json_schema(form);
}
v
}
}
pub fn find_command(id: &str) -> Option<&'static CommandSpec> {
commands().iter().find(|c| c.id == id)
}
+1 -1
View File
@@ -98,7 +98,7 @@ impl Backend for Headless {
}
fn commands(&mut self) -> Result<Value, String> {
Ok(fieldcraft_engine::commands().iter().map(|c| json!({"id": c.id, "label": c.label, "params": c.params})).collect())
Ok(fieldcraft_engine::commands().iter().map(fieldcraft_engine::CommandSpec::listing).collect())
}
fn picture(&mut self, r: &PictureRequest) -> Result<Picture, String> {
+3
View File
@@ -243,6 +243,9 @@ fn a_batch_stops_at_the_first_error() {
assert_eq!(ok(&mut s, "inspect_model", json!({}))["model"]["nodes"], 0);
let listed = ok(&mut s, "list_commands", json!({"filter": "SOLVE."}));
assert!(listed.as_array().unwrap().iter().all(|c| c["id"].as_str().unwrap().starts_with("solve.")));
// Each command's parameters as JSON Schema, for agents to build valid calls.
let statics = listed.as_array().unwrap().iter().find(|c| c["id"] == "solve.static").unwrap();
assert_eq!(statics["schema"]["properties"]["threads"]["maximum"], 256);
}
#[test]
+1 -3
View File
@@ -174,9 +174,7 @@ pub fn handle(app: &mut ModelApp, ctx: &egui::Context, req: &ControlRequest) ->
let Some(steps) = p.get("commands") else { return err("missing `commands` (a list of {command, params})") };
wrap(app.run_script(steps).map(|()| json!({"journal": app.session.journal.len()})))
}
"engine.commands" => {
ok(Value::Array(fieldcraft_engine::commands().iter().map(|c| json!({"id": c.id, "label": c.label, "params": c.params})).collect()))
}
"engine.commands" => ok(Value::Array(fieldcraft_engine::commands().iter().map(fieldcraft_engine::CommandSpec::listing).collect())),
"ui.inspect" => ok(json!({
"ui": app.ui,
"camera": app.cam,