mcp: insert_chart takes x, y, w and h

What was wrong: the insert_chart tool had no x, y, w or h in its schema, so the
server refused them as unknown arguments, while insert_picture and insert_table
take them. A chart could only be placed at the default box, [144, 81, 672, 378]
on a 960x540 slide, and then had to be moved with a second command.

What changed: insert_chart declares x, y, w and h and folds them into the
engine's `rect` when all four are given, as insert_picture and insert_table do.
Without all four the chart keeps its default place.

How it was tested: insert_chart_takes_a_box failed before the change (the
arguments were refused) and passes after it. It checks the box of the placed
chart, that a partial box leaves the default place, and that an extreme box is
answered without a panic. The rest of the deckcraft-mcp tests pass, and clippy
is clean with -D warnings.

Fixes #1151
This commit is contained in:
marshiyar
2026-10-30 20:13:00 -07:00
parent 4c1d77ddce
commit e83f964077
3 changed files with 44 additions and 4 deletions
+34
View File
@@ -62,6 +62,40 @@ fn handshake_and_tool_list() {
assert!(s.handle_line("{}").is_some());
}
/// #1151: `insert_chart` takes x, y, w and h like `insert_picture` and `insert_table`.
#[test]
fn insert_chart_takes_a_box() {
let mut s = Server::new(Box::new(Headless::new()));
tool(&mut s, "new_presentation", json!({"theme": "Harbor"}));
let chart = json!({"type": "pie", "categories": ["A", "B"], "series": [{"name": "S", "values": [3, 5]}]});
let box_of = |s: &mut Server, id: u64| {
let sl = tool(s, "inspect_slide", json!({}));
let shape = sl["shapes"].as_array().unwrap().iter().find(|x| x["id"].as_u64() == Some(id)).unwrap().clone();
assert_eq!(shape["kind"], "chart");
shape["box"].clone()
};
let mut args = chart.clone();
for (k, v) in [("x", 50), ("y", 60), ("w", 300), ("h", 200)] {
args[k] = json!(v);
}
let placed = tool(&mut s, "insert_chart", args)["id"].as_u64().unwrap();
assert_eq!(box_of(&mut s, placed), json!([50.0, 60.0, 300.0, 200.0]));
// without a full box the chart keeps its default place, as for pictures and tables
let plain = tool(&mut s, "insert_chart", chart.clone())["id"].as_u64().unwrap();
let default_box = box_of(&mut s, plain);
let mut partial = chart.clone();
partial["x"] = json!(50);
let partial_id = tool(&mut s, "insert_chart", partial)["id"].as_u64().unwrap();
assert_eq!(box_of(&mut s, partial_id), default_box);
// a hostile box is answered, not a panic
let mut odd = chart;
for (k, v) in [("x", json!(-1e300)), ("y", json!(1e300)), ("w", json!(-5)), ("h", json!(0))] {
odd[k] = v;
}
let r = call(&mut s, 11, "tools/call", json!({"name": "insert_chart", "arguments": odd}));
assert!(r["result"].is_object(), "{r}");
}
#[test]
fn agent_builds_a_deck() {
let mut s = Server::new(Box::new(Headless::new()));
+9 -3
View File
@@ -234,9 +234,9 @@ pub fn tool_definitions() -> Vec<Value> {
tool(
"insert_chart",
"Insert chart",
"Insert a chart (column, bar, line, pie, doughnut, area, scatter, stackedColumn…) with categories and series.",
"Insert a chart (column, bar, line, pie, doughnut, area, scatter, stackedColumn…) with categories and series, optionally at a box.",
obj(
json!({"type": string("Chart type"), "title": string("Chart title"), "categories": {"type": "array", "items": {"type": "string"}}, "series": {"type": "array", "items": obj(json!({"name": string("Series name"), "values": {"type": "array", "items": {"type": "number"}}}), &["values"])}}),
json!({"type": string("Chart type"), "title": string("Chart title"), "categories": {"type": "array", "items": {"type": "string"}}, "series": {"type": "array", "items": obj(json!({"name": string("Series name"), "values": {"type": "array", "items": {"type": "number"}}}), &["values"])}, "x": num("Left"), "y": num("Top"), "w": num("Width"), "h": num("Height")}),
&[],
),
false,
@@ -444,7 +444,13 @@ pub fn call_tool(b: &mut dyn Backend, name: &str, a: &Value) -> ToolResult {
}
wrap(exec(b, "insert.table", p))
}
"insert_chart" => wrap(exec(b, "insert.chart", a)),
"insert_chart" => {
let mut p = a.clone();
if let (Some(x), Some(y), Some(w), Some(h)) = (f(&a, "x"), f(&a, "y"), f(&a, "w"), f(&a, "h")) {
p["rect"] = json!([x, y, w, h]);
}
wrap(exec(b, "insert.chart", p))
}
"select" => wrap(exec(b, "edit.select", a)),
"pointer" => wrap(b.call("ui.pointer", a)),
"select_tool" => wrap(b.call("ui.tool.select", a)),
+1 -1
View File
@@ -96,7 +96,7 @@ files it was given rather than guarding against other software on the computer.
| `screenshot` | The whole window, UI included (connected mode) |
| `call_app` | Any [control protocol](control-protocol.md) method (connected mode) |
A tool refuses an argument that is not in its schema (`insert_chart: unknown argument "data" (expected: categories, series, title, type)`). Keys inside `run_command`'s `params` are that command's parameters.
A tool refuses an argument that is not in its schema (`insert_chart: unknown argument "data" (expected: categories, h, series, title, type, w, x, y)`). Keys inside `run_command`'s `params` are that command's parameters.
Useful commands for building decks: `slide.new {layout, title, body}`, `shape.insert {preset,
rect, text}`, `shape.connect {from, to, preset}`, `shape.freeform {points, closed, smooth}`,