From 5c13f6ec72c14d2544c4a567d71527afbc627d2d Mon Sep 17 00:00:00 2001 From: LloydNicholson <34131158+LloydNicholson@users.noreply.github.com> Date: Sat, 10 Oct 2026 12:17:37 +0200 Subject: [PATCH] MCP: the export tool lists PDF and its layout options (#65) tools/list described export as png/jpeg/pptx/outline only, with no PDF parameters, although file.export (which the tool runs) writes PDF slides, notes pages and handouts. The tool's description now names PDF, format is an enum of the formats file.export writes, and the schema lists the PDF options: layout, perPage, dpi, slides, includeHidden, textLayer and frame. Co-authored-by: lloyd Co-authored-by: Claude Opus 5.5 --- crates/mcp/src/tests.rs | 28 ++++++++++++++++++++++++++++ crates/mcp/src/tools.rs | 17 +++++++++++++++-- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/crates/mcp/src/tests.rs b/crates/mcp/src/tests.rs index e5a7ef2..b152152 100644 --- a/crates/mcp/src/tests.rs +++ b/crates/mcp/src/tests.rs @@ -89,3 +89,31 @@ fn errors_are_reported_not_panics() { 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); +} diff --git a/crates/mcp/src/tools.rs b/crates/mcp/src/tools.rs index abc791b..1a457dd 100644 --- a/crates/mcp/src/tools.rs +++ b/crates/mcp/src/tools.rs @@ -115,9 +115,22 @@ pub fn tool_definitions() -> Vec { tool( "export", "Export", - "Export slides: png/jpeg (one slide, or all with all:true), pptx, outline text.", + "Export slides: png/jpeg (one slide, or all with all:true), pptx, deckcraft, outline text, or pdf (slides, notes pages or handouts).", obj( - json!({"path": string("Output path"), "format": string("png|jpeg|pptx|deckcraft|outline"), "slide": int("Slide index for images"), "all": boolean("All slides (images)"), "scale": num("Pixels per point for images (default 2)")}), + json!({ + "path": string("Output path"), + "format": {"type": "string", "enum": ["png", "jpeg", "pptx", "deckcraft", "outline", "pdf"], "description": "Output format (default: from the path's extension)"}, + "slide": int("Slide index for images"), + "all": boolean("All slides (images)"), + "scale": num("Pixels per point for images (default 2)"), + "layout": {"type": "string", "enum": ["slides", "notes", "handouts"], "description": "PDF page layout (default slides)"}, + "perPage": {"type": "integer", "enum": [1, 2, 3, 4, 6, 9], "description": "PDF handouts: slides per page (default 6)"}, + "dpi": num("PDF: raster resolution of slide artwork, 36-600 (default 200)"), + "slides": {"type": "array", "items": {"type": "integer", "minimum": 0}, "description": "PDF: slide indices to export (default all)"}, + "includeHidden": boolean("PDF: include hidden slides (default false)"), + "textLayer": boolean("PDF: invisible text layer for search, select and copy (default true)"), + "frame": boolean("PDF notes/handouts: thin frame around each slide (default true)") + }), &["path"], ), false,