mcp: read object template via document.node summary flag

This commit is contained in:
Fahri Meral 2026-10-07 16:39:06 +03:00
parent d9136c46b9
commit e5e09d5d48
3 changed files with 43 additions and 27 deletions

View File

@ -25,7 +25,7 @@ pub fn specs() -> Vec<CommandSpec> {
cmd!("file.close", "Close", ["File"], Some("Cmd+W"), "{index?}", has_doc, file_close),
cmd!("document.activate", "Activate Document", [], None, "{index}", always, doc_activate),
cmd!(query "document.inspect", "Inspect Document", [], None, "{} → layer tree, artboards, selection, history", has_doc, |s, _| Ok(inspect::document(s))),
cmd!(query "document.node", "Inspect Object", [], None, "{id} → full object JSON", has_doc, doc_node),
cmd!(query "document.node", "Inspect Object", [], None, "{id, summary?: the compact document.inspect-style summary instead of the full object JSON} → one object", has_doc, doc_node),
cmd!(query "document.json", "Document JSON", [], None, "{} → complete document model", has_doc, |s, _| Ok(serde_json::to_value(&*s.doc()?.doc).unwrap_or(Value::Null))),
cmd!(
"document.setUnits",
@ -106,7 +106,7 @@ fn doc_activate(s: &mut Session, p: &Value) -> Result<Value> {
fn doc_node(s: &mut Session, p: &Value) -> Result<Value> {
let id = id_param(p, "id").ok_or_else(|| bad("document.node", "missing id"))?;
let n = s.doc()?.doc.node(id).ok_or(EngineError::NoNode(id))?;
Ok(serde_json::to_value(n).unwrap_or(Value::Null))
if bool_or(p, "summary", false) { Ok(inspect::node_summary(n)) } else { Ok(serde_json::to_value(n).unwrap_or(Value::Null)) }
}
/// Document Setup's units (also `document.setup {units}`).

View File

@ -285,6 +285,21 @@ fn inspect_lists_everything() {
assert!(s.commands().len() > 100);
}
/// `document.node {summary: true}` answers the `document.inspect` shape for one node:
/// the same fields the full-tree summary carries, without serializing the whole tree.
#[test]
fn document_node_summary_matches_inspect() {
let mut s = session();
let id = rect(&mut s, 0.0, 0.0, 10.0, 10.0).0;
let full = s.execute("document.node", &json!({"id": id})).unwrap();
assert!(full.get("bounds").is_none(), "full object JSON carries geometry, not bounds: {full}");
let summary = s.execute("document.node", &json!({"id": id, "summary": true})).unwrap();
let v = s.execute("document.inspect", &json!({})).unwrap();
let node = v["layers"][0]["children"].as_array().unwrap().iter().find(|n| n["id"] == id).cloned().unwrap();
assert_eq!(summary, node, "{summary} vs {node}");
assert!(s.execute("document.node", &json!({"id": 999999, "summary": true})).is_err());
}
/// Type reports the paint its characters show, and its own object-level paint apart from it.
#[test]
fn inspect_reports_the_paint_of_types_characters() {

View File

@ -44,7 +44,7 @@ pub static TEMPLATES: &[Template] = &[
uri: "vectorcraft://object/{id}",
name: "object",
title: "One layer or object",
description: "The layer or object with this id, with its children, bounds and paint (document.inspect)",
description: "The layer or object with this id, with its children, bounds and paint (document.node {summary: true})",
mime: "application/json",
live: Live::Objects,
},
@ -118,7 +118,9 @@ pub fn read(b: &mut dyn Backend, uri: &str) -> Result<Value, ReadError> {
return Err(ReadError::Invalid(format!("{} needs a value for `{}`", template.uri, variable_name(template.uri))));
}
let found = match template.name {
"object" => find_object(b, &var).map_err(ReadError::Backend)?,
// One direct `document.node` lookup through `engine.execute` (which reaches both
// backends), instead of pulling the whole layer tree out of `document.inspect`.
"object" => read_object(b, &var).map_err(ReadError::Backend)?,
// The command catalogue is a control method; the rest are engine commands.
"command" => find_in(b.call("engine.commands", json!({})).map_err(ReadError::Backend)?, None, "id", &var),
"effect" => find_in(exec(b, "effect.list").map_err(ReadError::Backend)?, Some("catalog"), "id", &var),
@ -171,30 +173,23 @@ fn decode(s: &str) -> String {
String::from_utf8_lossy(&out).into_owned()
}
/// The layer or object with this id in the active document, with its subtree.
fn find_object(b: &mut dyn Backend, id: &str) -> Result<Option<Value>, String> {
let doc = exec(b, "document.inspect")?;
Ok(find_node(doc.get("layers"), id))
}
/// Depth-first search of the layer tree for a node whose `id` matches.
fn find_node(node: Option<&Value>, id: &str) -> Option<Value> {
let nodes = node?.as_array()?;
let mut stack: Vec<&Value> = nodes.iter().rev().collect();
while let Some(n) = stack.pop() {
let hit = match n.get("id") {
Some(Value::String(s)) => s == id,
Some(Value::Number(number)) => number.to_string() == id,
_ => false,
};
if hit {
return Some(n.clone());
}
if let Some(kids) = n.get("children").and_then(Value::as_array) {
stack.extend(kids.iter().rev());
}
/// The layer or object with this id in the active document, as its compact summary
/// (bounds, paint labels, subtree — the `document.inspect` shape for one node).
///
/// One `document.node {summary: true}` lookup through `engine.execute` (which reaches
/// both backends), instead of pulling the whole layer tree out of `document.inspect`
/// and scanning it: the reply matches the old read field for field, without the
/// O(document) serialize-and-summarize on every call.
///
/// A non-numeric id, or one that names nothing, is "not found" (the caller turns it into
/// `-32602`); backend failures are swallowed the same way, matching how [`crate::prompts`]
/// treats an unreachable catalogue (empty rather than an error).
fn read_object(b: &mut dyn Backend, id: &str) -> Result<Option<Value>, String> {
let Ok(num) = id.parse::<u64>() else { return Ok(None) };
match b.call("engine.execute", json!({"command": "document.node", "params": {"id": num, "summary": true}})) {
Ok(v) => Ok(Some(v)),
Err(_) => Ok(None),
}
None
}
/// The item in a query command's array whose `field` equals `value`.
@ -261,5 +256,11 @@ mod tests {
let v = read(b.as_mut(), &format!("vectorcraft://object/{id}")).unwrap();
assert_eq!(v["id"].to_string(), id);
assert!(v["bounds"].is_object(), "{v}");
// Layers resolve too, not just drawn objects.
let inspect = b.call("document.inspect", json!({})).unwrap();
let layer = inspect["layers"].as_array().and_then(|l| l.first()).expect("a layer");
let lid = layer["id"].to_string();
let v = read(b.as_mut(), &format!("vectorcraft://object/{lid}")).unwrap();
assert_eq!(v["id"].to_string(), lid);
}
}