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 fn value(&self) -> &Value {
32 &self.value
33 }
34
35 pub(super) fn visited_nodes(&self) -> u64 {
36 self.visited_nodes
37 }
38}
39
40impl serde::Serialize for McpResponse {
41 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
42 self.value.serialize(serializer)
43 }
44}
45impl<'de> serde::Deserialize<'de> for McpResponse {
46 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
47 let mut value = Value::deserialize(deserializer)?;
48 Self::take(&mut value)
49 .map_err(|_| serde::de::Error::custom("recorded MCP response exceeds its bounds"))
50 }
51}
52
53struct TreeBudget {
56 bytes: usize,
57 nodes: u64,
58}
59
60impl TreeBudget {
61 fn visit(&mut self, value: &Value, depth: usize) -> bool {
62 if depth >= MAX_DEPTH || !self.node() {
63 return false;
64 }
65 match value {
66 Value::Null | Value::Bool(_) | Value::Number(_) => true,
67 Value::String(value) => self.bytes(value.len()),
68 Value::Array(values) => values.iter().all(|value| self.visit(value, depth + 1)),
69 Value::Object(fields) => fields.iter().all(|(key, value)| {
70 self.node() && self.bytes(key.len()) && self.visit(value, depth + 1)
71 }),
72 }
73 }
74
75 fn node(&mut self) -> bool {
76 self.nodes = self.nodes.saturating_add(1);
77 self.nodes <= MAX_NODES && self.bytes(16)
78 }
79
80 fn bytes(&mut self, bytes: usize) -> bool {
81 self.bytes = self.bytes.saturating_add(bytes);
82 self.bytes <= MCP_MAX_RESPONSE_BYTES
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn admission_moves_the_tree_and_counts_payload_and_nodes() {
92 let mut value = serde_json::json!({ "x": ["a\nb", null] });
93 let mut budget = TreeBudget { bytes: 0, nodes: 0 };
94 assert!(budget.visit(&value, 0));
95 assert_eq!(budget.bytes, 5 * 16 + 1 + 3);
96 assert_eq!(budget.nodes, 5);
97 let response = McpResponse::take(&mut value).unwrap();
98 assert!(value.is_null());
99 assert_eq!(response.value()["x"][0], "a\nb");
100 }
101
102 #[test]
103 fn admission_rejects_depth_and_node_limits_without_taking_ownership() {
104 let mut nested = Value::Null;
105 for _ in 0..MAX_DEPTH {
106 nested = Value::Array(vec![nested]);
107 }
108 assert!(matches!(
109 McpResponse::take(&mut nested),
110 Err(McpCallError::ResponseTooLarge)
111 ));
112 assert!(nested.is_array());
113 let mut wide = Value::Array(vec![Value::Null; MAX_NODES as usize]);
114 assert!(McpResponse::take(&mut wide).is_err());
115 }
116}