MCP: reject document.node slices without summary, document and sweep them (review of #405)

`depth` / `childLimit` without `summary: true` used to return the whole
full-JSON subtree silently; it is now an invalid-params error. docs/mcp.md
describes the slice parameters and childCount, the engine test covers
fractional values, unsliced reads and limits past the tree, and the CLI
stdio sweep reads a sliced group through run_command.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic
2026-10-08 07:43:07 +07:00
co-authored by Claude Opus 5.5
parent 81ab5cb3b0
commit 526fc2e157
4 changed files with 44 additions and 3 deletions
+8 -1
View File
@@ -105,8 +105,15 @@ fn doc_activate(s: &mut Session, p: &Value) -> Result<Value> {
fn doc_node(s: &mut Session, p: &Value) -> Result<Value> {
let id = id_param(p, "id").ok_or_else(|| bad("document.node", "missing id"))?;
let opts = slice_opts(p)?;
let summary = bool_or(p, "summary", false);
if !summary && (opts.depth.is_some() || opts.child_limit.is_some()) {
// The full object JSON is never truncated: a slice without `summary` would
// otherwise silently return the whole subtree.
return Err(bad("document.node", "`depth` and `childLimit` slice the summary: pass `summary: true`"));
}
let n = s.doc()?.doc.node(id).ok_or(EngineError::NoNode(id))?;
if bool_or(p, "summary", false) { Ok(inspect::node_summary_opts(n, slice_opts(p)?)) } else { Ok(serde_json::to_value(n).unwrap_or(Value::Null)) }
if summary { Ok(inspect::node_summary_opts(n, opts)) } else { Ok(serde_json::to_value(n).unwrap_or(Value::Null)) }
}
/// `depth` / `childLimit` for a summary read: absent (or null) means no limit.
+10 -1
View File
@@ -381,6 +381,8 @@ fn document_node_summary_slices_with_depth_and_limit() {
let g = read(json!({"id": outer, "summary": true}));
assert_eq!(g["children"].as_array().map(Vec::len), Some(2));
assert!(g.get("childCount").is_none(), "nothing truncated: {g}");
// Limits beyond the tree are the whole tree, byte for byte.
assert_eq!(read(json!({"id": outer, "summary": true, "depth": u64::MAX, "childLimit": u64::MAX})), g);
let one = read(json!({"id": outer, "summary": true, "childLimit": 1}));
assert_eq!(one["children"].as_array().map(Vec::len), Some(1));
@@ -400,7 +402,14 @@ fn document_node_summary_slices_with_depth_and_limit() {
assert!(nested.get("children").is_none(), "{nested}");
assert_eq!(nested["childCount"], 2);
for bad in [json!({"id": outer, "summary": true, "depth": -1}), json!({"id": outer, "summary": true, "childLimit": "many"})] {
for bad in [
json!({"id": outer, "summary": true, "depth": -1}),
json!({"id": outer, "summary": true, "depth": 1.5}),
json!({"id": outer, "summary": true, "childLimit": "many"}),
// A slice of the full object JSON would silently be the whole subtree.
json!({"id": outer, "depth": 0}),
json!({"id": outer, "summary": false, "childLimit": 1}),
] {
assert!(s.execute("document.node", &bad).is_err(), "{bad}");
}
}