From 38c62bda3e2ea7375c1bcb735ee83b989a61108a Mon Sep 17 00:00:00 2001 From: munzir Date: Thu, 8 Oct 2026 04:14:16 +0300 Subject: [PATCH] Automation: file-writing tools no longer advertise read-only (#130) (#138) doc_export_data, accessibility_report and image_save each write to their required path and replace an existing file there, but the tool table marked all three read-only, so MCP clients were told the calls change no file. They now carry the destructive flag the table defines for a tool that overwrites files, and a test pins the three names. Co-authored-by: Mohammed Munzir Co-authored-by: Brandon Thomas --- crates/automation/src/tools.rs | 6 +++--- crates/automation/tests/automation.rs | 11 +++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/crates/automation/src/tools.rs b/crates/automation/src/tools.rs index 21828c84e..e6a793595 100644 --- a/crates/automation/src/tools.rs +++ b/crates/automation/src/tools.rs @@ -307,7 +307,7 @@ pub fn tools() -> Vec { &["doc"], )), t("doc_export_data", "Export comments or form data", "Write comments and/or form data to path; the extension picks the format: .xfdf or .fdf (comments and/or fields), .xml, .csv or .txt (form data). what: all (default), comments, fields.") - .ro() + .destructive() .with(schema(json!({ "doc": doc(), "path": { "type": "string" }, "what": { "type": "string", "enum": ["all", "comments", "fields"] } }), &["doc", "path"])), t("doc_import_data", "Import comments or form data", "Import comments and/or field values from an XFDF, FDF, XML, CSV or tab-delimited text file (detected from its content). Comments with the same name are replaced; values go through the form's formats and validation. Undoable.") .with(schema(json!({ "doc": doc(), "path": { "type": "string" } }), &["doc", "path"])), @@ -720,7 +720,7 @@ pub fn tools() -> Vec { &["doc"], )), t("accessibility_report", "Accessibility report", "Run the full check and write the accessibility report (HTML) to path; returns the results too. Takes the same options as accessibility_check.") - .ro() + .destructive() .cmd("a11y.report") .with(schema( json!({ @@ -943,7 +943,7 @@ pub fn tools() -> Vec { &["doc", "page", "image", "action"], )), t("image_save", "Save image as", "Write one of a page's images to path: JPEG images unchanged, others as PNG (the extension is added when missing).") - .ro() + .destructive() .cmd("edit.edit_text") .with(schema(json!({ "doc": doc(), "page": { "type": "integer", "minimum": 1 }, "image": { "type": "integer", "minimum": 1 }, "path": { "type": "string" } }), &["doc", "page", "image", "path"])), t("text_paragraphs", "List paragraphs", "The paragraphs on a page (lines grouped by font, size, alignment and spacing): number, text, its line numbers, box (top-left-origin points), font and size. Use the number with text_edit's paragraph.") diff --git a/crates/automation/tests/automation.rs b/crates/automation/tests/automation.rs index e200bfd45..f92a14dac 100644 --- a/crates/automation/tests/automation.rs +++ b/crates/automation/tests/automation.rs @@ -82,6 +82,17 @@ fn tool_table_is_well_formed() { } } +/// Each of these writes to its required `path` and replaces an existing file there, so the MCP +/// annotations must not tell clients the call is read-only or harmless (#130). +#[test] +fn file_writing_tools_are_not_read_only() { + for name in ["doc_export_data", "accessibility_report", "image_save"] { + let t = tools().into_iter().find(|t| t.name == name).unwrap(); + assert!(!t.read_only, "{name} writes a file but advertises read-only"); + assert!(t.destructive, "{name} overwrites its path but advertises non-destructive"); + } +} + #[test] fn open_inspect_render_and_find() { let dir = workdir("inspect");