fix(automation): no top-level oneOf in form_set_props input schema (#1961) (#1967)

Anthropic's tool API rejects a request when any tool's input_schema has
oneOf, anyOf or allOf at the top level, so attaching `pdfcraft-cli mcp`
to a Claude-based client made every prompt fail. The field/fields rule is
already enforced in the form_set_props handler and described in the
schema, so the oneOf is dropped.

tool_table_is_well_formed now checks that no tool has a top-level
oneOf/anyOf/allOf.

Co-authored-by: Eromosele <188490945+andreero0@users.noreply.github.com>
This commit is contained in:
Eromosele
2026-10-15 02:34:20 -04:00
committed by GitHub
co-authored by Eromosele
parent 8e803b6290
commit 9c42c0c5fe
2 changed files with 5 additions and 6 deletions
+2 -6
View File
@@ -372,8 +372,7 @@ pub fn tools() -> Vec<ToolDef> {
"Field properties",
"Change field properties: give field for one field, or fields for a list of unique field names (1–1000). A bulk change is atomic and one Undo step; an invalid or locked field leaves every field unchanged. Only the given properties change, and partial appearance changes preserve each field's other colours and font. name (rename), rect (position) and rotation require a single field. General and Options: tooltip, read_only, required, multiline, max_length (0 = no limit), options (combo/list items), font_size (0 = auto), format, validate, calculate (Acrobat's native AF rules). Options: align (left, center, right), default (Reset form's value; check boxes/radio buttons use their on state, \"\" for off), flags { scroll, rich_text, password, file_select, spell_check, comb (needs max_length), sort, editable, multi_select, commit_immediately }, check_style (check boxes/radio buttons: check, circle, cross, diamond, square, star). rotation is 0, 90, 180 or 270 degrees counterclockwise and swaps width/height when the axis changes. Undoable.",
)
.with({
let mut input = schema(
.with(schema(
json!({
"doc": doc(),
"field": { "type": "string", "description": "The field's current name." },
@@ -399,10 +398,7 @@ pub fn tools() -> Vec<ToolDef> {
"calculate": { "description": "Calculate tab: {\"op\": \"sum\"|\"product\"|\"average\"|\"min\"|\"max\", \"fields\": [\"a\", \"b\"]}, {\"notation\": \"Price * Qty\"} (simplified field notation), or \"none\"." },
}),
&["doc"],
);
input["oneOf"] = json!([{ "required": ["field"] }, { "required": ["fields"] }]);
input
}),
)),
t("form_tab_order", "Set the tab order", "Set the tab order of pages (default all): row (top to bottom, left to right), column, structure, or annotations (unspecified). Or order tabs manually: `field` with `move` earlier|later moves that field one place on its page. Returns the resulting order of fields. Undoable.")
.with(schema(
json!({ "doc": doc(), "order": { "type": "string", "enum": ["row", "column", "structure", "annotations"] }, "pages": pages("to set (default: all)"), "field": { "type": "string" }, "move": { "type": "string", "enum": ["earlier", "later"] } }),
+3
View File
@@ -238,6 +238,9 @@ fn tool_table_is_well_formed() {
assert!(props.contains_key(r.as_str().unwrap()), "{}: required {r} is not a property", t.name);
}
assert!(!(t.read_only && t.destructive), "{} is both read-only and destructive", t.name);
for k in ["oneOf", "anyOf", "allOf"] {
assert!(t.input_schema.get(k).is_none(), "{}: top-level {k} in input_schema is rejected by MCP clients such as Claude", t.name);
}
if let Some(c) = t.command {
assert!(pdfcraft_engine::commands::command(c).is_some(), "{} names unregistered command {c}", t.name);
}