1use crate::denial_code::DenialCode;
5use serde_json::Value;
6
7#[derive(Debug, Clone, PartialEq)]
9pub struct ToolCallRequest {
10 pub id: Option<Value>,
12 pub tool: String,
14 pub arguments: Value,
16}
17
18#[derive(Debug, thiserror::Error, PartialEq, Eq)]
20#[non_exhaustive]
21pub enum RpcError {
22 #[error("invalid JSON: {0}")]
24 Json(String),
25 #[error("not a JSON-RPC 2.0 request: {0}")]
27 NotJsonRpc(String),
28 #[error("method {0:?} is not tools/call")]
31 NotToolCall(String),
32 #[error("invalid tools/call params: {0}")]
34 BadParams(String),
35 #[error("payload of {0} bytes exceeds the {1}-byte bound")]
37 TooLarge(usize, usize),
38}
39
40pub const MAX_PAYLOAD_BYTES: usize = 4 * 1024 * 1024;
42
43pub const MAX_JSON_DEPTH: usize = 64;
45
46pub fn parse_tool_call(raw: &[u8]) -> Result<ToolCallRequest, RpcError> {
48 if raw.len() > MAX_PAYLOAD_BYTES {
49 return Err(RpcError::TooLarge(raw.len(), MAX_PAYLOAD_BYTES));
50 }
51 reject_duplicate_keys(raw)?;
64 let v: Value = serde_json::from_slice(raw).map_err(|e| RpcError::Json(e.to_string()))?;
65 if depth_of(&v, 0) > MAX_JSON_DEPTH {
66 return Err(RpcError::NotJsonRpc("nesting exceeds depth bound".into()));
67 }
68 let obj = v
69 .as_object()
70 .ok_or_else(|| RpcError::NotJsonRpc("root is not an object".into()))?;
71 match obj.get("jsonrpc").and_then(Value::as_str) {
72 Some("2.0") => {}
73 other => {
74 return Err(RpcError::NotJsonRpc(format!(
75 "jsonrpc field is {other:?}, need \"2.0\""
76 )))
77 }
78 }
79 let method = obj
80 .get("method")
81 .and_then(Value::as_str)
82 .ok_or_else(|| RpcError::NotJsonRpc("method missing or not a string".into()))?;
83 if method != "tools/call" {
84 return Err(RpcError::NotToolCall(method.to_owned()));
85 }
86 let params = obj
87 .get("params")
88 .and_then(Value::as_object)
89 .ok_or_else(|| RpcError::BadParams("params missing or not an object".into()))?;
90 let tool = params
91 .get("name")
92 .and_then(Value::as_str)
93 .filter(|s| !s.is_empty())
94 .ok_or_else(|| RpcError::BadParams("params.name missing or empty".into()))?;
95 if av_core::text::contains_bidi_or_zero_width(tool) {
96 return Err(RpcError::BadParams(
97 "params.name carries a bidi/zero-width spoofing character".into(),
98 ));
99 }
100 if tool.chars().any(|c| c.is_control() || c.is_whitespace()) {
107 return Err(RpcError::BadParams(
108 "params.name contains a control character or whitespace".into(),
109 ));
110 }
111 if !tool.is_ascii() {
123 return Err(RpcError::BadParams(
124 "params.name must be ASCII: non-ASCII tool names introduce a Unicode-normalization mismatch \
125 between this proxy's exact-byte matching and downstream MCP servers that fold NFC/NFKC"
126 .into(),
127 ));
128 }
129 if tool.chars().any(|c| c.is_ascii_uppercase()) {
136 return Err(RpcError::BadParams(
137 "params.name must be lowercase: mixed-case tool names bypass policy deny-lists that use \
138 exact-byte matching while downstream MCP servers apply ASCII case folding"
139 .into(),
140 ));
141 }
142 let id = obj.get("id").cloned();
146 match id.as_ref() {
147 None => {}
148 Some(Value::String(_) | Value::Number(_) | Value::Null) => {}
149 Some(_) => {
150 return Err(RpcError::BadParams(
151 "id must be a string, number, or null per JSON-RPC 2.0 §4".into(),
152 ));
153 }
154 }
155 if id.is_none() {
160 return Err(RpcError::BadParams(
161 "tools/call requires an id; JSON-RPC notifications are not accepted".into(),
162 ));
163 }
164 let arguments = params
165 .get("arguments")
166 .cloned()
167 .unwrap_or_else(|| Value::Object(Default::default()));
168 if !arguments.is_object() {
169 return Err(RpcError::BadParams("params.arguments must be an object".into()));
170 }
171 Ok(ToolCallRequest {
172 id,
173 tool: tool.to_owned(),
174 arguments,
175 })
176}
177
178pub fn depth_of(v: &Value, current: usize) -> usize {
183 if current > MAX_JSON_DEPTH {
184 return current; }
186 match v {
187 Value::Array(items) => items
188 .iter()
189 .map(|i| depth_of(i, current + 1))
190 .max()
191 .unwrap_or(current + 1),
192 Value::Object(map) => map
193 .values()
194 .map(|i| depth_of(i, current + 1))
195 .max()
196 .unwrap_or(current + 1),
197 _ => current,
198 }
199}
200
201pub fn refuse_duplicate_json_keys(raw: &[u8]) -> Result<(), String> {
210 reject_duplicate_keys(raw).map_err(|error| error.to_string())
211}
212
213fn reject_duplicate_keys(raw: &[u8]) -> Result<(), RpcError> {
236 use serde::de::{Deserializer, MapAccess, SeqAccess, Visitor};
237 use std::fmt;
238
239 const DUP_KEY_SENTINEL: &str = "__av_sandbox_dup:";
240
241 struct NoDupKeys;
242
243 impl<'de> Visitor<'de> for NoDupKeys {
244 type Value = ();
245
246 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247 f.write_str("any JSON value without duplicate object keys")
248 }
249
250 fn visit_bool<E>(self, _: bool) -> Result<(), E> {
251 Ok(())
252 }
253 fn visit_i64<E>(self, _: i64) -> Result<(), E> {
254 Ok(())
255 }
256 fn visit_u64<E>(self, _: u64) -> Result<(), E> {
257 Ok(())
258 }
259 fn visit_f64<E>(self, _: f64) -> Result<(), E> {
260 Ok(())
261 }
262 fn visit_str<E>(self, _: &str) -> Result<(), E> {
263 Ok(())
264 }
265 fn visit_string<E>(self, _: String) -> Result<(), E> {
266 Ok(())
267 }
268 fn visit_none<E>(self) -> Result<(), E> {
269 Ok(())
270 }
271 fn visit_unit<E>(self) -> Result<(), E> {
272 Ok(())
273 }
274
275 fn visit_some<D>(self, deserializer: D) -> Result<(), D::Error>
276 where
277 D: Deserializer<'de>,
278 {
279 deserializer.deserialize_any(NoDupKeys)
280 }
281
282 fn visit_seq<A>(self, mut seq: A) -> Result<(), A::Error>
283 where
284 A: SeqAccess<'de>,
285 {
286 while seq.next_element::<NoDupWrap>()?.is_some() {}
287 Ok(())
288 }
289
290 fn visit_map<A>(self, mut map: A) -> Result<(), A::Error>
291 where
292 A: MapAccess<'de>,
293 {
294 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
295 while let Some(key) = map.next_key::<String>()? {
296 if !seen.insert(key.clone()) {
297 return Err(serde::de::Error::custom(format!(
298 "{DUP_KEY_SENTINEL}duplicate object key `{}`",
299 key.escape_debug()
300 )));
301 }
302 let _: NoDupWrap = map.next_value()?;
303 }
304 Ok(())
305 }
306 }
307
308 struct NoDupWrap;
310 impl<'de> serde::Deserialize<'de> for NoDupWrap {
311 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
312 where
313 D: Deserializer<'de>,
314 {
315 deserializer.deserialize_any(NoDupKeys)?;
316 Ok(NoDupWrap)
317 }
318 }
319
320 let mut de = serde_json::Deserializer::from_slice(raw);
321 if let Err(error) = de.deserialize_any(NoDupKeys) {
322 let msg = error.to_string();
323 return if let Some(rest) = msg.strip_prefix(DUP_KEY_SENTINEL) {
324 Err(RpcError::Json(format!("duplicate JSON key rejected: {rest}")))
325 } else {
326 Err(RpcError::Json(msg))
331 };
332 }
333 de.end()
337 .map_err(|error| RpcError::Json(format!("trailing content after JSON value: {error}")))?;
338 Ok(())
339}
340
341pub fn authorization_error(id: Option<&Value>, reason: &str) -> Value {
344 serde_json::json!({
345 "jsonrpc": "2.0",
346 "id": id.cloned().unwrap_or(Value::Null),
347 "error": {
348 "code": -32001,
349 "message": "tool call blocked by AgentVisor AI policy",
350 "data": { "reason": reason }
351 }
352 })
353}
354
355pub fn denial_error(id: Option<&Value>, code: DenialCode, reason: &str) -> Value {
359 serde_json::json!({
360 "jsonrpc": "2.0",
361 "id": id.cloned().unwrap_or(Value::Null),
362 "error": {
363 "code": -32001,
364 "message": "tool call blocked by AgentVisor AI policy",
365 "data": { "reason": reason, "code": code.as_str() }
366 }
367 })
368}
369
370#[must_use]
382pub fn detectable_error_id(raw: &[u8], error: &RpcError) -> Option<Value> {
383 if !matches!(error, RpcError::NotToolCall(_) | RpcError::BadParams(_)) {
384 return None;
385 }
386 let value: Value = serde_json::from_slice(raw).ok()?;
390 let id = value.as_object()?.get("id")?;
391 matches!(id, Value::String(_) | Value::Number(_) | Value::Null).then(|| id.clone())
393}
394
395pub fn protocol_error(id: Option<&Value>, error: &RpcError) -> Value {
401 let (code, message) = match error {
402 RpcError::Json(_) => (-32700, "parse error"),
403 RpcError::NotJsonRpc(_) | RpcError::TooLarge(..) => (-32600, "invalid request"),
404 RpcError::BadParams(_) => (-32602, "invalid params"),
405 RpcError::NotToolCall(_) => {
407 return denial_error(id, DenialCode::PassthroughRefused, &error.to_string());
408 }
409 };
410 serde_json::json!({
411 "jsonrpc": "2.0",
412 "id": id.cloned().unwrap_or(Value::Null),
413 "error": {
414 "code": code,
415 "message": message,
416 "data": { "reason": error.to_string(), "code": DenialCode::ParseError.as_str() }
417 }
418 })
419}
420
421#[cfg(test)]
422mod tests {
423 #![allow(
424 clippy::unwrap_used,
425 clippy::expect_used,
426 clippy::panic,
427 clippy::indexing_slicing
428 )]
429
430 use super::*;
431 use proptest::prelude::*;
432 use serde_json::json;
433
434 fn call(tool: &str, args: Value) -> Vec<u8> {
435 serde_json::to_vec(&json!({
436 "jsonrpc": "2.0",
437 "id": 7,
438 "method": "tools/call",
439 "params": { "name": tool, "arguments": args }
440 }))
441 .unwrap()
442 }
443
444 #[test]
450 fn refuse_duplicate_json_keys_covers_nesting_and_trailing_garbage() {
451 assert!(refuse_duplicate_json_keys(br#"{"a":1,"a":2}"#).is_err());
452 assert!(refuse_duplicate_json_keys(br#"{"outer":{"a":1,"a":2}}"#).is_err());
453 assert!(refuse_duplicate_json_keys(br#"{"arr":[{"a":1,"a":2}]}"#).is_err());
454 assert!(refuse_duplicate_json_keys(br#"{"ok":1}garbage"#).is_err());
455 assert!(refuse_duplicate_json_keys(br#"{"a":1,"b":{"a":1},"c":[1,"a"]}"#).is_ok());
456 assert!(refuse_duplicate_json_keys(b"[1,2,3]").is_ok());
457 }
458
459 #[test]
465 fn protocol_error_pins_reserved_jsonrpc_codes() {
466 let cases = [
467 (RpcError::Json("x".into()), -32700),
468 (RpcError::NotJsonRpc("x".into()), -32600),
469 (RpcError::TooLarge(9, 1), -32600),
470 (RpcError::BadParams("x".into()), -32602),
471 ];
472 for (error, expected) in cases {
473 let response = protocol_error(None, &error);
474 assert_eq!(response["error"]["code"].as_i64(), Some(expected), "{error:?}");
475 }
476 let passthrough = protocol_error(None, &RpcError::NotToolCall("initialize".into()));
477 assert_eq!(passthrough["error"]["code"].as_i64(), Some(-32001));
478 }
479
480 #[test]
487 fn error_responses_echo_a_detectable_request_id() {
488 let raw = br#"{"jsonrpc":"2.0","id":42,"method":"initialize","params":{}}"#;
490 let error = parse_tool_call(raw).unwrap_err();
491 assert!(matches!(error, RpcError::NotToolCall(_)));
492 let id = detectable_error_id(raw, &error);
493 assert_eq!(id, Some(json!(42)));
494 let response = protocol_error(id.as_ref(), &error);
495 assert_eq!(response["id"], json!(42));
496
497 let raw = br#"{"jsonrpc":"2.0","id":"req-1","method":"tools/call","params":{}}"#;
499 let error = parse_tool_call(raw).unwrap_err();
500 assert!(matches!(error, RpcError::BadParams(_)));
501 assert_eq!(detectable_error_id(raw, &error), Some(json!("req-1")));
502
503 let raw = br#"{"jsonrpc":"2.0","id":{"nested":1},"method":"tools/call","params":{"name":"t"}}"#;
506 let error = parse_tool_call(raw).unwrap_err();
507 assert!(matches!(error, RpcError::BadParams(_)));
508 assert_eq!(detectable_error_id(raw, &error), None);
509
510 let raw = b"not json";
512 let error = parse_tool_call(raw).unwrap_err();
513 assert_eq!(detectable_error_id(raw, &error), None);
514 let response = protocol_error(None, &error);
515 assert_eq!(response["id"], Value::Null);
516 }
517
518 #[test]
519 fn parses_valid_call() {
520 let req = parse_tool_call(&call("db_write", json!({"table": "users"}))).unwrap();
521 assert_eq!(req.tool, "db_write");
522 assert_eq!(req.arguments["table"], "users");
523 assert_eq!(req.id, Some(json!(7)));
524 }
525
526 #[test]
527 fn missing_arguments_defaults_to_empty_object() {
528 let raw = serde_json::to_vec(&json!({
529 "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "t"}
530 }))
531 .unwrap();
532 assert_eq!(parse_tool_call(&raw).unwrap().arguments, json!({}));
533 }
534
535 #[test]
536 fn rejects_malformed_shapes() {
537 assert!(matches!(parse_tool_call(b"not json"), Err(RpcError::Json(_))));
538 assert!(matches!(
539 parse_tool_call(b"[1,2,3]"),
540 Err(RpcError::NotJsonRpc(_))
541 ));
542 assert!(matches!(
543 parse_tool_call(br#"{"jsonrpc":"1.0","method":"tools/call"}"#),
544 Err(RpcError::NotJsonRpc(_))
545 ));
546 assert!(matches!(
547 parse_tool_call(br#"{"jsonrpc":"2.0"}"#),
548 Err(RpcError::NotJsonRpc(_))
549 ));
550 assert!(matches!(
551 parse_tool_call(br#"{"jsonrpc":"2.0","method":"resources/read"}"#),
552 Err(RpcError::NotToolCall(_))
553 ));
554 assert!(matches!(
555 parse_tool_call(br#"{"jsonrpc":"2.0","method":"tools/call"}"#),
556 Err(RpcError::BadParams(_))
557 ));
558 assert!(matches!(
559 parse_tool_call(br#"{"jsonrpc":"2.0","method":"tools/call","params":{"name":""}}"#),
560 Err(RpcError::BadParams(_))
561 ));
562 assert!(matches!(
563 parse_tool_call(
564 br#"{"jsonrpc":"2.0","method":"tools/call","params":{"name":"t","arguments":[1]}}"#
565 ),
566 Err(RpcError::BadParams(_))
567 ));
568 }
569
570 #[test]
571 fn oversized_payload_rejected_before_parse() {
572 let huge = vec![b'x'; MAX_PAYLOAD_BYTES + 1];
573 assert!(matches!(parse_tool_call(&huge), Err(RpcError::TooLarge(..))));
574 }
575
576 #[test]
577 fn max_payload_bytes_is_pinned_at_4_mib() {
578 assert_eq!(MAX_PAYLOAD_BYTES, 4_194_304);
582 }
583
584 #[test]
585 fn deep_nesting_rejected() {
586 let mut nested = String::from("1");
587 for _ in 0..200 {
588 nested = format!("[{nested}]");
589 }
590 let raw = format!(
591 r#"{{"jsonrpc":"2.0","method":"tools/call","params":{{"name":"t","arguments":{{"x":{nested}}}}}}}"#
592 );
593 assert!(parse_tool_call(raw.as_bytes()).is_err());
594 }
595
596 #[test]
597 fn authorization_error_shape() {
598 let e = authorization_error(Some(&json!(9)), "budget exceeded");
599 assert_eq!(e["id"], 9);
600 assert_eq!(e["error"]["code"], -32001);
601 assert_eq!(e["error"]["data"]["reason"], "budget exceeded");
602 }
603
604 #[test]
605 fn denial_error_carries_machine_readable_code() {
606 let e = denial_error(
607 Some(&json!("req-7")),
608 DenialCode::BudgetExceeded,
609 "budget exceeded",
610 );
611 assert_eq!(e["jsonrpc"], "2.0");
612 assert_eq!(e["id"], "req-7");
613 assert_eq!(e["error"]["code"], -32001);
614 assert_eq!(e["error"]["data"]["reason"], "budget exceeded");
615 assert_eq!(e["error"]["data"]["code"], "BUDGET_EXCEEDED");
616 }
617
618 #[test]
619 fn protocol_errors_carry_denial_codes() {
620 let parse = protocol_error(None, &RpcError::Json("bad".into()));
621 assert_eq!(parse["error"]["code"], -32700);
622 assert_eq!(parse["error"]["data"]["code"], "PARSE_ERROR");
623 let passthrough = protocol_error(Some(&json!(3)), &RpcError::NotToolCall("tools/list".into()));
624 assert_eq!(passthrough["id"], 3);
625 assert_eq!(passthrough["error"]["code"], -32001);
626 assert_eq!(passthrough["error"]["data"]["code"], "PASSTHROUGH_REFUSED");
627 }
628
629 proptest! {
630 #![proptest_config(ProptestConfig::with_cases(256))]
631
632 #[test]
634 fn never_panics(bytes in prop::collection::vec(any::<u8>(), 0..2048)) {
635 let _ = parse_tool_call(&bytes);
636 }
637
638 #[test]
640 fn never_panics_on_arbitrary_json(s in "\\PC{0,500}") {
641 let _ = parse_tool_call(s.as_bytes());
642 }
643 }
644
645 #[test]
651 fn tool_name_carrying_a_bidi_or_zero_width_character_is_rejected() {
652 for spoof in [
653 "db_write\u{202E}etirw_bd",
654 "\u{202E}db_write",
655 "db\u{200B}_write",
656 "db_write\u{200E}",
657 "db_write\u{2066}suffix",
658 "db_write\u{FEFF}",
659 ] {
660 let raw = call(spoof, json!({}));
661 match parse_tool_call(&raw) {
662 Err(RpcError::BadParams(reason)) => {
663 assert!(reason.contains("spoofing"), "wrong reason: {reason}");
664 }
665 other => panic!("must reject {spoof:?}, got {other:?}"),
666 }
667 }
668 }
669
670 #[test]
677 fn tool_name_with_control_char_or_whitespace_is_rejected() {
678 for hostile in [
679 "db_write\n",
680 "db_write\r",
681 "db_write\t",
682 "db_write\0",
683 "db_write ",
684 " db_write",
685 "db write",
686 "db_write\u{000B}",
687 "db_write\x7f",
688 ] {
689 let raw = call(hostile, json!({}));
690 match parse_tool_call(&raw) {
691 Err(RpcError::BadParams(reason)) => {
692 assert!(
693 reason.contains("control character or whitespace"),
694 "wrong reason: {reason}",
695 );
696 }
697 other => panic!("must reject {hostile:?}, got {other:?}"),
698 }
699 }
700 }
701
702 #[test]
707 fn tools_call_without_id_is_rejected_as_a_notification() {
708 let raw = serde_json::to_vec(&json!({
709 "jsonrpc": "2.0",
710 "method": "tools/call",
711 "params": {"name": "safe_tool", "arguments": {}}
712 }))
713 .unwrap();
714 match parse_tool_call(&raw) {
715 Err(RpcError::BadParams(reason)) => {
716 assert!(reason.contains("notification"), "wrong reason: {reason}");
717 }
718 other => panic!("notification must be rejected, got {other:?}"),
719 }
720 }
721
722 #[test]
726 fn tools_call_with_structured_id_is_rejected() {
727 for hostile_id in [
728 json!({"nested": true}),
729 json!([1, 2, 3]),
730 json!(true),
731 json!(false),
732 ] {
733 let raw = serde_json::to_vec(&json!({
734 "jsonrpc": "2.0",
735 "id": hostile_id,
736 "method": "tools/call",
737 "params": {"name": "safe_tool", "arguments": {}}
738 }))
739 .unwrap();
740 match parse_tool_call(&raw) {
741 Err(RpcError::BadParams(reason)) => {
742 assert!(reason.contains("JSON-RPC 2.0"), "wrong reason: {reason}");
743 }
744 other => panic!("hostile id {hostile_id:?} must be rejected, got {other:?}"),
745 }
746 }
747 }
748
749 #[test]
751 fn valid_id_shapes_are_accepted() {
752 for good_id in [json!("uuid-123"), json!(42), json!(-7), json!(null)] {
753 let raw = serde_json::to_vec(&json!({
754 "jsonrpc": "2.0",
755 "id": good_id,
756 "method": "tools/call",
757 "params": {"name": "safe_tool", "arguments": {}}
758 }))
759 .unwrap();
760 let parsed = parse_tool_call(&raw).unwrap_or_else(|e| panic!("{good_id:?}: {e:?}"));
761 assert_eq!(parsed.id, Some(good_id));
762 }
763 }
764
765 #[test]
771 fn tools_call_with_uppercase_letters_in_name_is_rejected() {
772 for hostile in ["DB_write", "Delete", "readFile", "PING"] {
773 let raw = serde_json::to_vec(&json!({
774 "jsonrpc": "2.0",
775 "id": "case-attack",
776 "method": "tools/call",
777 "params": {"name": hostile, "arguments": {}}
778 }))
779 .unwrap();
780 let err = parse_tool_call(&raw).unwrap_err();
781 assert!(
782 matches!(err, RpcError::BadParams(ref msg) if msg.contains("lowercase")),
783 "expected lowercase-refusal for {hostile:?}, got {err:?}"
784 );
785 }
786 }
787
788 #[test]
794 fn tools_call_with_non_ascii_bytes_in_name_is_rejected() {
795 for hostile in [
796 "d\u{0301}elete",
797 "de\u{0301}lete",
798 "délete",
799 "𝐝𝐛_𝐰𝐫𝐢𝐭𝐞",
800 "read_file",
801 ] {
802 let raw = serde_json::to_vec(&json!({
803 "jsonrpc": "2.0",
804 "id": "unicode-attack",
805 "method": "tools/call",
806 "params": {"name": hostile, "arguments": {}}
807 }))
808 .unwrap();
809 let err = parse_tool_call(&raw).unwrap_err();
810 assert!(
811 matches!(err, RpcError::BadParams(ref msg) if msg.contains("ASCII")),
812 "expected ASCII-only refusal for {hostile:?}, got {err:?}"
813 );
814 }
815 }
816 #[test]
825 fn duplicate_key_class_distinguished_from_generic_parse_error() {
826 let real_dup = serde_json::to_vec(&json!({
828 "jsonrpc": "2.0",
829 "id": "dup",
830 "method": "tools/call",
831 "params": {"name": "read", "arguments": {}}
832 }))
833 .unwrap();
834 let dup_raw: Vec<u8> = {
837 let mut v = real_dup.clone();
838 let insertion = br#""jsonrpc":"2.0","#;
841 v.splice(1..1, insertion.iter().copied());
842 v
843 };
844 let err = parse_tool_call(&dup_raw).unwrap_err();
845 assert!(
846 matches!(&err, RpcError::Json(msg) if msg.contains("duplicate JSON key rejected")),
847 "real dup-key must carry the dup-key prefix, got {err:?}"
848 );
849
850 let malformed: &[u8] = b"{\"jsonrpc\":\"2.0\",\"id\":1,";
852 let err = parse_tool_call(malformed).unwrap_err();
853 match &err {
854 RpcError::Json(msg) => assert!(
855 !msg.contains("duplicate JSON key rejected"),
856 "malformed JSON must not be labeled as duplicate-key, got {msg:?}"
857 ),
858 other => panic!("expected RpcError::Json for malformed input, got {other:?}"),
859 }
860
861 let err = parse_tool_call(b"").unwrap_err();
863 if let RpcError::Json(msg) = &err {
864 assert!(
865 !msg.contains("duplicate JSON key rejected"),
866 "empty input must not be labeled as duplicate-key, got {msg:?}"
867 );
868 }
869 }
870
871 #[test]
879 fn trailing_garbage_after_valid_json_is_refused() {
880 let valid = serde_json::to_vec(&json!({
882 "jsonrpc": "2.0",
883 "id": "trail",
884 "method": "tools/call",
885 "params": {"name": "read", "arguments": {}}
886 }))
887 .unwrap();
888 let mut with_trail = valid.clone();
889 with_trail.extend_from_slice(b"garbage");
890 let err = parse_tool_call(&with_trail).unwrap_err();
891 assert!(
892 matches!(&err, RpcError::Json(msg) if msg.contains("trailing content")),
893 "trailing bytes must be refused, got {err:?}"
894 );
895 let mut with_ws = valid.clone();
898 with_ws.extend_from_slice(b" \n\t");
899 assert!(
900 parse_tool_call(&with_ws).is_ok(),
901 "trailing whitespace must be allowed"
902 );
903 }
904}
905
906#[cfg(test)]
907mod depth_boundary_tests {
908 #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
909
910 use super::*;
911
912 #[test]
920 fn json_depth_boundary_is_exact_for_arrays_and_objects() {
921 let build = |inner: String| {
922 format!(
923 r#"{{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{{"name":"t","arguments":{{"k":{inner}}}}}}}"#
924 )
925 };
926 let arrays = |n: usize| format!("{}0{}", "[".repeat(n), "]".repeat(n));
927 let objects = |n: usize| format!("{}0{}", "{\"x\":".repeat(n), "}".repeat(n));
928 for shape in [&arrays as &dyn Fn(usize) -> String, &objects] {
929 let at_limit = build(shape(MAX_JSON_DEPTH - 3));
930 assert!(
931 parse_tool_call(at_limit.as_bytes()).is_ok(),
932 "depth exactly at MAX_JSON_DEPTH must parse"
933 );
934 let past = build(shape(MAX_JSON_DEPTH - 2));
935 let outcome = parse_tool_call(past.as_bytes());
936 assert!(
937 matches!(outcome, Err(RpcError::NotJsonRpc(ref m)) if m.contains("depth")),
938 "one past MAX_JSON_DEPTH must be refused, got {outcome:?}"
939 );
940 }
941 for leaf in ["{}", "[]"] {
945 let wrappers = "{\"x\":".repeat(MAX_JSON_DEPTH - 3);
946 let closers = "}".repeat(MAX_JSON_DEPTH - 3);
947 let past = build(format!("{wrappers}{leaf}{closers}"));
948 let outcome = parse_tool_call(past.as_bytes());
949 assert!(
950 matches!(outcome, Err(RpcError::NotJsonRpc(ref m)) if m.contains("depth")),
951 "empty {leaf} leaf past MAX_JSON_DEPTH must be refused, got {outcome:?}"
952 );
953 }
954 }
955}