//! An agent builds a small deck entirely through MCP and checks its work only through MCP. use serde_json::{Value, json}; use crate::{Headless, Server}; fn call(s: &mut Server, id: u64, method: &str, params: Value) -> Value { let line = json!({"jsonrpc": "2.0", "id": id, "method": method, "params": params}).to_string(); let out = s.handle_line(&line).expect("a response"); serde_json::from_str(&out).expect("json") } fn tool(s: &mut Server, name: &str, args: Value) -> Value { let r = call(s, 9, "tools/call", json!({"name": name, "arguments": args})); let res = &r["result"]; assert_eq!(res["isError"], false, "{name} failed: {res}"); let text = res["content"].as_array().unwrap().iter().find(|c| c["type"] == "text").map(|c| c["text"].as_str().unwrap().to_string()).unwrap_or_default(); serde_json::from_str(&text).unwrap_or(Value::String(text)) } #[test] fn handshake_and_tool_list() { let mut s = Server::new(Box::new(Headless::new())); let r = call(&mut s, 1, "initialize", json!({"protocolVersion": "2025-06-18"})); assert_eq!(r["result"]["serverInfo"]["name"], "deckcraft"); let tools = call(&mut s, 2, "tools/list", json!({})); let names: Vec<&str> = tools["result"]["tools"].as_array().unwrap().iter().map(|t| t["name"].as_str().unwrap()).collect(); for n in ["run_command", "render_slide", "add_slide", "add_shape", "pointer", "inspect_slide"] { assert!(names.contains(&n), "{n}"); } // Garbage never panics. assert!(s.handle_line("not json").is_some()); assert!(s.handle_line("{}").is_some()); } #[test] fn agent_builds_a_deck() { let mut s = Server::new(Box::new(Headless::new())); tool(&mut s, "new_presentation", json!({"theme": "Harbor"})); let doc = tool(&mut s, "inspect_document", json!({})); let title_id = { let sl = tool(&mut s, "inspect_slide", json!({})); sl["shapes"][0]["id"].as_u64().unwrap() }; let _ = doc; tool(&mut s, "set_text", json!({"id": title_id, "text": "Quarterly Review"})); tool(&mut s, "add_slide", json!({"layout": "titleAndContent", "title": "Highlights", "body": "Revenue up\n\tNew markets\nCosts down"})); let shape = tool(&mut s, "add_shape", json!({"preset": "roundRect", "x": 600, "y": 300, "w": 200, "h": 100, "text": "Goal", "fill": "accent2"})); let sid = shape["id"].as_u64().unwrap(); tool(&mut s, "run_command", json!({"command": "shape.rotate", "params": {"id": sid, "deg": 10}})); // Drag the shape with the pointer. tool( &mut s, "pointer", json!({"events": [{"kind": "down", "x": 700, "y": 350}, {"kind": "drag", "x": 650, "y": 330}, {"kind": "up", "x": 650, "y": 330}]}), ); tool(&mut s, "insert_chart", json!({"type": "pie", "categories": ["A", "B"], "series": [{"name": "S", "values": [3, 5]}]})); let sl = tool(&mut s, "inspect_slide", json!({})); assert_eq!(sl["title"], "Highlights"); let shapes = sl["shapes"].as_array().unwrap(); let goal = shapes.iter().find(|x| x["id"].as_u64() == Some(sid)).unwrap(); assert_eq!(goal["text"], "Goal"); assert_eq!(goal["rotation"], 10.0); assert!(goal["box"][0].as_f64().unwrap() < 600.0, "moved left: {goal}"); assert!(shapes.iter().any(|x| x["kind"] == "chart")); let doc = tool(&mut s, "inspect_document", json!({})); assert_eq!(doc["slides"].as_array().unwrap().len(), 2); assert_eq!(doc["theme"], "Harbor"); // Look at it. let r = call(&mut s, 10, "tools/call", json!({"name": "render_slide", "arguments": {"scale": 0.25}})); assert_eq!(r["result"]["content"][0]["type"], "image"); // Keyboard: select the shape, delete it, undo. tool(&mut s, "select", json!({"ids": [sid]})); tool(&mut s, "key", json!({"key": "Delete"})); let n1 = tool(&mut s, "inspect_slide", json!({}))["shapes"].as_array().unwrap().len(); tool(&mut s, "key", json!({"key": "z", "mods": {"cmd": true}})); let n2 = tool(&mut s, "inspect_slide", json!({}))["shapes"].as_array().unwrap().len(); assert_eq!(n2, n1 + 1); } #[test] fn errors_are_reported_not_panics() { let mut s = Server::new(Box::new(Headless::new())); let r = call(&mut s, 1, "tools/call", json!({"name": "run_command", "arguments": {"command": "no.such"}})); assert_eq!(r["result"]["isError"], true); let r = call(&mut s, 2, "tools/call", json!({"name": "screenshot", "arguments": {}})); assert_eq!(r["result"]["isError"], true); let r = call(&mut s, 3, "tools/call", json!({"name": "add_shape", "arguments": {"preset": "rect"}})); assert_eq!(r["result"]["isError"], true); } #[test] fn export_tool_lists_pdf_and_its_options() { let mut s = Server::new(Box::new(Headless::new())); let tools = call(&mut s, 1, "tools/list", json!({})); let export = tools["result"]["tools"].as_array().unwrap().iter().find(|t| t["name"] == "export").unwrap().clone(); assert!(export["description"].as_str().unwrap().contains("pdf"), "{export}"); let props = &export["inputSchema"]["properties"]; let formats = props["format"]["enum"].as_array().unwrap(); for f in ["png", "jpeg", "pptx", "deckcraft", "outline", "pdf"] { assert!(formats.iter().any(|x| x == f), "format {f}: {export}"); } assert_eq!(props["layout"]["enum"], json!(["slides", "notes", "handouts"])); assert_eq!(props["perPage"]["enum"], json!([1, 2, 3, 4, 6, 9])); for p in ["dpi", "slides", "includeHidden", "textLayer", "frame"] { assert!(props.get(p).is_some(), "{p}: {export}"); } // What the schema describes is accepted. let dir = std::env::temp_dir().join(format!("deckcraft-mcp-export-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); tool(&mut s, "new_presentation", json!({})); for layout in ["slides", "notes", "handouts"] { let path = dir.join(format!("{layout}.pdf")); tool(&mut s, "export", json!({"path": path.to_string_lossy(), "format": "pdf", "layout": layout, "perPage": 3})); assert!(std::fs::read(&path).unwrap().starts_with(b"%PDF"), "{layout}"); } let _ = std::fs::remove_dir_all(&dir); }