submilli_engine/runtime/mcp/
response.rs1use serde_json::Value;
2
3use super::{MCP_MAX_RESPONSE_BYTES, McpCallError};
4
5const MAX_DEPTH: usize = 128;
6const MAX_NODES: u64 = 100_000;
7
8#[derive(Debug)]
11pub struct McpResponse {
12 value: Value,
13 visited_nodes: u64,
14}
15
16impl McpResponse {
17 pub fn take(value: &mut Value) -> Result<Self, McpCallError> {
21 let mut budget = TreeBudget { bytes: 0, nodes: 0 };
22 if !budget.visit(value, 0) {
23 return Err(McpCallError::ResponseTooLarge);
24 }
25 Ok(Self {
26 value: value.take(),
27 visited_nodes: budget.nodes,
28 })
29 }
30
31 pub(super) fn value(&self) -> &Value {
32 &self.value
33 }
34
35 pub(super) fn visited_nodes(&self) -> u64 {
36 self.visited_nodes
37 }
38}
39
40struct TreeBudget {
43 bytes: usize,
44 nodes: u64,
45}
46
47impl TreeBudget {
48 fn visit(&mut self, value: &Value, depth: usize) -> bool {
49 if depth >= MAX_DEPTH || !self.node() {
50 return false;
51 }
52 match value {
53 Value::Null | Value::Bool(_) | Value::Number(_) => true,
54 Value::String(value) => self.bytes(value.len()),
55 Value::Array(values) => values.iter().all(|value| self.visit(value, depth + 1)),
56 Value::Object(fields) => fields.iter().all(|(key, value)| {
57 self.node() && self.bytes(key.len()) && self.visit(value, depth + 1)
58 }),
59 }
60 }
61
62 fn node(&mut self) -> bool {
63 self.nodes = self.nodes.saturating_add(1);
64 self.nodes <= MAX_NODES && self.bytes(16)
65 }
66
67 fn bytes(&mut self, bytes: usize) -> bool {
68 self.bytes = self.bytes.saturating_add(bytes);
69 self.bytes <= MCP_MAX_RESPONSE_BYTES
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 #[test]
78 fn admission_moves_the_tree_and_counts_payload_and_nodes() {
79 let mut value = serde_json::json!({ "x": ["a\nb", null] });
80 let mut budget = TreeBudget { bytes: 0, nodes: 0 };
81 assert!(budget.visit(&value, 0));
82 assert_eq!(budget.bytes, 5 * 16 + 1 + 3);
83 assert_eq!(budget.nodes, 5);
84 let response = McpResponse::take(&mut value).unwrap();
85 assert!(value.is_null());
86 assert_eq!(response.value()["x"][0], "a\nb");
87 }
88
89 #[test]
90 fn admission_rejects_depth_and_node_limits_without_taking_ownership() {
91 let mut nested = Value::Null;
92 for _ in 0..MAX_DEPTH {
93 nested = Value::Array(vec![nested]);
94 }
95 assert!(matches!(
96 McpResponse::take(&mut nested),
97 Err(McpCallError::ResponseTooLarge)
98 ));
99 assert!(nested.is_array());
100 let mut wide = Value::Array(vec![Value::Null; MAX_NODES as usize]);
101 assert!(McpResponse::take(&mut wide).is_err());
102 }
103}