MCP: an integer given as a string or a fraction is refused as the wrong type, not reported as missing

A command that needs a whole number of 0 or more, such as `index` of
`appearance.setItem`, `anchor` of `path.removeAnchor` and
`addon.pen.removePoint`, `sourceDocument` of `edit.dropSelection`, `index`
of `document.activate` and `to` of `document.move`, answered "missing
integer `index`" (or "missing index") both when the key was left out and
when it was given as a value that is not a whole number of 0 or more, such
as "3", 2.5, -1 or true. A value that is given but is not one is now
refused with "`index` must be a whole number", the message object ids
already use. A key left out or given as null is still missing, and an
optional index such as `subpath` keeps its default. Neither error changes
the document or records an undo step.

Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-11-13 18:02:42 -08:00
parent 71cef8ac8d
commit 78ce56d021
7 changed files with 91 additions and 13 deletions
+3 -3
View File
@@ -36,7 +36,7 @@ use super::paint::paint_from;
use super::{AppearanceAttrs, EyedropperAttrs, FillAttrs, StrokeAttrs};
use super::{
CommandSpec, NodeId, Result, Session, bad, check_numbers, check_switches, cmd, has_doc, has_selection, ids_param, index, leaf_targets,
object_param, ok, paint_targets, str_param, switch_or, targets,
object_param, ok, paint_targets, str_param, switch_or, targets, u64_req,
};
use crate::EngineError;
@@ -604,9 +604,9 @@ pub(crate) fn edits_stroke(s: &Session, p: &Value, item: ItemTarget, default: bo
Ok(ids.get(item.anchor(s, &ids)).and_then(|id| d.node(*id)?.appearance.items.get(index)).map_or(default, |it| !it.is_fill()))
}
/// The whole number of 0 or more that `key` holds, read with [`index`].
/// The whole number of 0 or more that `key` holds, read with [`u64_req`] and [`index`].
pub(crate) fn index_param(p: &Value, key: &str, cmd: &str) -> Result<usize> {
p.get(key).and_then(Value::as_u64).map(index).ok_or_else(|| bad(cmd, format!("missing integer `{key}`")))
u64_req(p, key, cmd).map(index)
}
fn set_active_item(s: &mut Session, p: &Value) -> Result<Value> {
+2 -2
View File
@@ -231,9 +231,9 @@ pub(super) fn within_input_bound(q: Point) -> bool {
q.x.is_finite() && q.y.is_finite() && q.x.abs() < MAX_INPUT && q.y.abs() < MAX_INPUT
}
/// The whole number of 0 or more that `key` holds, read with [`index`].
/// The whole number of 0 or more that `key` holds, read with [`u64_req`](crate::cmd::u64_req) and [`index`].
pub(super) fn usize_req(p: &Value, key: &str, cmd: &str) -> Result<usize> {
p.get(key).and_then(Value::as_u64).map(index).ok_or_else(|| bad(cmd, format!("missing integer `{key}`")))
crate::cmd::u64_req(p, key, cmd).map(index)
}
/// The whole number of 0 or more that `key` holds, or `None` when `key` is absent or null. Any other
+10 -5
View File
@@ -11,7 +11,7 @@ use super::clipboard::SwatchChoices;
use super::{
CommandSpec, Result, Session, always, bad, can_clear_history, can_redo, can_undo, check_numbers, checked_ids_param, clock_date, cmd, date_param,
f64_req, finite_numbers, has_clipboard, has_doc, has_selection, has_selection_guides_or_artboard_tool, has_selection_or_guides, index, number_or,
object_param, ok, point_param, str_param, switch_or,
object_param, ok, point_param, str_param, switch_or, u64_req,
};
use crate::{Clipboard, EngineError, SourceLayer, inspect};
@@ -249,7 +249,10 @@ fn file_close(s: &mut Session, p: &Value) -> Result<Value> {
}
fn doc_activate(s: &mut Session, p: &Value) -> Result<Value> {
let i = p.get("index").and_then(Value::as_u64).map(index).ok_or_else(|| bad("document.activate", "missing index"))?;
let i = match p.get("index") {
None | Some(Value::Null) => return Err(bad("document.activate", "missing index")),
Some(v) => v.as_u64().map(index).ok_or_else(|| bad("document.activate", "`index` must be a whole number"))?,
};
if s.set_active(i) { ok() } else { Err(bad("document.activate", "no such document")) }
}
@@ -262,7 +265,10 @@ fn doc_move(s: &mut Session, p: &Value) -> Result<Value> {
if from >= n {
return Err(bad("document.move", "no such document"));
}
let to = p.get("to").and_then(Value::as_u64).map(index).ok_or_else(|| bad("document.move", "missing `to`: a non-negative integer"))?;
let to = match p.get("to") {
None | Some(Value::Null) => return Err(bad("document.move", "missing `to`: a non-negative integer")),
Some(v) => v.as_u64().map(index).ok_or_else(|| bad("document.move", "`to` must be a whole number"))?,
};
let to = to.min(n.saturating_sub(1));
if !s.move_document(from, to) {
return Err(bad("document.move", "no such document"));
@@ -1190,8 +1196,7 @@ fn clear(s: &mut Session, p: &Value) -> Result<Value> {
/// Document ids scope node ids: two tabs may contain objects with the same node id.
fn drop_selection(s: &mut Session, p: &Value) -> Result<Value> {
const CMD: &str = "edit.dropSelection";
let key = "sourceDocument";
let uid = p[key].as_u64().ok_or_else(|| bad(CMD, format!("missing integer `{key}`")))?;
let uid = u64_req(p, "sourceDocument", CMD)?;
let point = |key: &str| finite_numbers::<2>(&p[key]).ok_or_else(|| bad(CMD, format!("{key} must be [x, y] with finite coordinates")));
let grab = point("grab")?;
let at = point("at")?;