diff --git a/crates/mcp/src/resources.rs b/crates/mcp/src/resources.rs index 704c3392e..50792de6d 100644 --- a/crates/mcp/src/resources.rs +++ b/crates/mcp/src/resources.rs @@ -188,6 +188,10 @@ fn decode(s: &str) -> String { /// caller turns it into `-32602`); other failures (no document, an unreachable app) stay /// backend errors. fn read_object(b: &mut dyn Backend, id: &str) -> Result, String> { + // `parse` alone would accept a leading `+`. + if id.is_empty() || !id.bytes().all(|c| c.is_ascii_digit()) { + return Ok(None); + } let Ok(id) = id.parse::() else { return Ok(None) }; match b.call("engine.execute", json!({"command": "document.node", "params": {"id": id, "summary": true}})) { Ok(v) => Ok(Some(v)), @@ -268,6 +272,17 @@ mod tests { assert!(matches!(read(empty.as_mut(), "vectorcraft://object/1"), Err(ReadError::Backend(_)))); } + #[test] + fn object_id_must_be_plain_digits() { + let mut b = Box::new(crate::Headless::with_document()); + let made = b.call("engine.execute", json!({"command": "shape.rectangle", "params": {"x": 10, "y": 10, "width": 50, "height": 40}})).unwrap(); + let id = made["id"].as_i64().expect("shape.rectangle returns an integer id"); + for uri in [format!("vectorcraft://object/+{id}"), format!("vectorcraft://object/-{id}")] { + assert!(matches!(read(b.as_mut(), &uri), Err(ReadError::Invalid(_))), "{uri}"); + } + assert!(read(b.as_mut(), &format!("vectorcraft://object/{id}")).is_ok()); + } + #[test] fn an_object_is_readable_once_it_exists() { let mut b = Box::new(crate::Headless::with_document());