MCP: headless open_file reads .pdf and PDF-compatible .ai, as File ▸ Open does

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 <[email protected]>
This commit is contained in:
storybored379andClaude Opus 5.5 committed 2026-10-04 01:17:34 -07:00
1 parent 604c20ed90
commit 1fd9b79a9d
5 files changed
+24 -3

No files matched your search

Generated
+1
View File
@@ -4185,6 +4185,7 @@ dependencies = [
"vectorcraft-engine",
"vectorcraft-format",
"vectorcraft-geom",
"vectorcraft-pdf",
"vectorcraft-render",
"vectorcraft-svg",
"vectorcraft-testkit",
+1
View File
@@ -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 }
+9 -2
View File
@@ -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<Value, String> {
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}))
+12
View File
@@ -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);
+1 -1
View File
@@ -246,7 +246,7 @@ pub fn tool_definitions() -> Vec<Value> {
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,
),