diff --git a/crates/mcp/src/tests.rs b/crates/mcp/src/tests.rs index bfffa83d..66559187 100644 --- a/crates/mcp/src/tests.rs +++ b/crates/mcp/src/tests.rs @@ -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())); diff --git a/crates/mcp/src/tools.rs b/crates/mcp/src/tools.rs index c4f7e85d..2bf09b8a 100644 --- a/crates/mcp/src/tools.rs +++ b/crates/mcp/src/tools.rs @@ -234,9 +234,9 @@ pub fn tool_definitions() -> Vec { 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)), diff --git a/docs/mcp.md b/docs/mcp.md index f936af66..7da2ffb3 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -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}`,