From 1fd9b79a9d01a8252888e34770515db47abaf4cf Mon Sep 17 00:00:00 2001 From: storybored379 Date: Sat, 3 Oct 2026 23:45:57 -0700 Subject: [PATCH] =?UTF-8?q?MCP:=20headless=20open=5Ffile=20reads=20.pdf=20?= =?UTF-8?q?and=20PDF-compatible=20.ai,=20as=20File=20=E2=96=B8=20Open=20do?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Headless `open_file` (and `file.open` through run_command) had its own loader that stopped at .vectorcraft and .svg, so an agent could not open a PDF or an .ai file without the desktop app, although the engine and `vectorcraft-cli` already import them through vectorcraft-pdf. The headless loader now imports .pdf, .ai and any file starting with %PDF through vectorcraft_pdf::import_with_report (default options, as File ▸ Open uses), and returns the import warnings like the SVG path. The open_file tool description lists the new types. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 1 + crates/mcp/Cargo.toml | 1 + crates/mcp/src/headless.rs | 11 +++++++++-- crates/mcp/src/tests.rs | 12 ++++++++++++ crates/mcp/src/tools.rs | 2 +- 5 files changed, 24 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 62c7c9d7..ec97e895 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4185,6 +4185,7 @@ dependencies = [ "vectorcraft-engine", "vectorcraft-format", "vectorcraft-geom", + "vectorcraft-pdf", "vectorcraft-render", "vectorcraft-svg", "vectorcraft-testkit", diff --git a/crates/mcp/Cargo.toml b/crates/mcp/Cargo.toml index 7234d53e..419150c0 100644 --- a/crates/mcp/Cargo.toml +++ b/crates/mcp/Cargo.toml @@ -12,6 +12,7 @@ workspace = true vectorcraft-engine = { workspace = true } vectorcraft-render = { workspace = true } vectorcraft-svg = { workspace = true } +vectorcraft-pdf = { workspace = true } vectorcraft-format = { workspace = true } vectorcraft-tools = { workspace = true } vectorcraft-geom = { workspace = true } diff --git a/crates/mcp/src/headless.rs b/crates/mcp/src/headless.rs index eacb1d76..31623c40 100644 --- a/crates/mcp/src/headless.rs +++ b/crates/mcp/src/headless.rs @@ -215,7 +215,7 @@ impl Headless { } } - /// `app.open {path}`: `.vectorcraft` or `.svg` as a new active document. + /// `app.open {path}`: `.vectorcraft`, `.svg`, `.pdf` or PDF-compatible `.ai` as a new active document. pub fn open(&mut self, p: &Value) -> Result { let path = s(p, "path").ok_or("missing `path`")?; let bytes = std::fs::read(path).map_err(|e| format!("read {path}: {e}"))?; @@ -234,8 +234,15 @@ impl Headless { d.title = name.clone(); warnings = w; (d, false) + } else if e == "pdf" || e == "ai" || bytes.starts_with(b"%PDF") { + // PDF, or the PDF-compatible part of an Illustrator `.ai`, as in the app's File ▸ Open. + let r = vectorcraft_pdf::import_with_report(&bytes, &vectorcraft_pdf::ImportOptions::default()).map_err(|e| e.to_string())?; + let mut d = r.document; + d.title = name.clone(); + warnings = r.warnings; + (d, false) } else { - return Err(format!("headless mode can open .vectorcraft and .svg files, not .{e}")); + return Err(format!("headless mode can open .vectorcraft, .svg, .pdf and .ai files, not .{e}")); }; let index = self.session.add_document(doc, keep_path.then(|| path.to_string())); Ok(json!({"index": index, "title": name, "warnings": warnings})) diff --git a/crates/mcp/src/tests.rs b/crates/mcp/src/tests.rs index f625104a..74d12b33 100644 --- a/crates/mcp/src/tests.rs +++ b/crates/mcp/src/tests.rs @@ -200,6 +200,18 @@ fn headless_end_to_end() { let r = call(&mut s, 9, "open_file", json!({"path": svg_path.to_str().unwrap()})); assert_eq!(r["isError"], false, "{r}"); + // PDF and PDF-compatible .ai open headless too. + let pdf_path = tmp("out.pdf"); + assert_eq!(call(&mut s, 20, "export", json!({"path": pdf_path.to_str().unwrap()}))["isError"], false); + let ai_path = tmp("out.ai"); + std::fs::copy(&pdf_path, &ai_path).unwrap(); + for (id, p) in [(21, &pdf_path), (22, &ai_path)] { + let r = call(&mut s, id, "open_file", json!({"path": p.to_str().unwrap()})); + assert_eq!(r["isError"], false, "{r}"); + let doc: Value = serde_json::from_str(&text_of(&call(&mut s, id + 10, "inspect_document", json!({})))).unwrap(); + assert!(doc["objects"].as_u64().unwrap() >= 2, "{doc}"); + } + // Resources. let v = rpc(&mut s, 10, "resources/list", json!({})); assert_eq!(v["result"]["resources"].as_array().unwrap().len(), 2); diff --git a/crates/mcp/src/tools.rs b/crates/mcp/src/tools.rs index 09b90610..34c10bb6 100644 --- a/crates/mcp/src/tools.rs +++ b/crates/mcp/src/tools.rs @@ -246,7 +246,7 @@ pub fn tool_definitions() -> Vec { tool( "open_file", "Open file", - "Open a .vectorcraft or .svg file as a new, active document.", + "Open a .vectorcraft, .svg, .pdf or PDF-compatible .ai file as a new, active document.", obj(json!({"path": string("File path")}), &["path"]), false, ),