33
44//! JSON-RPC 2.0 over HTTP L7 inspection.
55
6- use miette:: Result ;
7- use sha2:: { Digest , Sha256 } ;
8- use std:: collections:: BTreeMap ;
96use std:: collections:: HashMap ;
7+
8+ use miette:: Result ;
109use tokio:: io:: { AsyncRead , AsyncWrite } ;
1110use tower_mcp_types:: protocol:: {
1211 JSONRPC_VERSION , JsonRpcNotification , JsonRpcRequest , McpNotification , McpRequest ,
@@ -123,28 +122,6 @@ impl JsonRpcRequestInfo {
123122 error : None ,
124123 }
125124 }
126-
127- /// Logs store only a digest of params. For batches, hash the per-call
128- /// canonical maps so denied-call logging cannot leak raw argument values.
129- pub ( crate ) fn params_sha256 ( & self ) -> Option < String > {
130- if self . is_batch {
131- if self . calls . is_empty ( ) || self . calls . iter ( ) . all ( |call| call. params . is_empty ( ) ) {
132- return None ;
133- }
134- let canonical_params = self
135- . calls
136- . iter ( )
137- . map ( |call| canonical_params_map ( & call. params ) )
138- . collect :: < Vec < _ > > ( ) ;
139- return Some ( sha256_json ( & canonical_params) ) ;
140- }
141-
142- let call = self . calls . first ( ) ?;
143- if call. params . is_empty ( ) {
144- return None ;
145- }
146- Some ( sha256_json ( & canonical_params_map ( & call. params ) ) )
147- }
148125}
149126
150127pub ( crate ) fn jsonrpc_receive_stream_request ( request : & L7Request ) -> bool {
@@ -437,18 +414,6 @@ fn validate_mcp_tool_name(name: &str) -> std::result::Result<(), String> {
437414 Ok ( ( ) )
438415}
439416
440- fn canonical_params_map ( params : & HashMap < String , String > ) -> BTreeMap < String , String > {
441- params
442- . iter ( )
443- . map ( |( key, value) | ( key. clone ( ) , value. clone ( ) ) )
444- . collect ( )
445- }
446-
447- fn sha256_json ( value : & impl serde:: Serialize ) -> String {
448- let encoded = serde_json:: to_vec ( value) . expect ( "canonical JSON-RPC params should serialize" ) ;
449- hex:: encode ( Sha256 :: digest ( & encoded) )
450- }
451-
452417fn flatten_json_value (
453418 prefix : & str ,
454419 path_segments : usize ,
@@ -511,10 +476,10 @@ fn insert_flattened_param(
511476 out. insert ( key. to_string ( ) , param) ;
512477 Ok ( ( ) )
513478}
514-
515479#[ cfg( test) ]
516480mod tests {
517481 use super :: * ;
482+ use std:: collections:: HashMap ;
518483
519484 #[ test]
520485 fn parses_method_from_request_body ( ) {
@@ -539,7 +504,6 @@ mod tests {
539504 assert ! ( !info. is_batch) ;
540505 assert ! ( info. has_response) ;
541506 assert ! ( info. error. is_none( ) ) ;
542- assert ! ( info. params_sha256( ) . is_none( ) ) ;
543507 }
544508
545509 #[ test]
@@ -554,9 +518,14 @@ mod tests {
554518 }
555519
556520 #[ test]
557- fn flattens_object_params_for_policy_matching ( ) {
521+ fn ignores_params_when_extracting_method ( ) {
558522 let body = br#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"submit_report","arguments":{"scope":"workspace/main"}}}"# ;
559523 let info = parse_jsonrpc_body ( body, JsonRpcInspectionMode :: JsonRpc ) ;
524+ assert ! ( info. error. is_none( ) ) ;
525+ assert_eq ! (
526+ info. calls. first( ) . map( |call| call. method. as_str( ) ) ,
527+ Some ( "tools/call" )
528+ ) ;
560529 let params = & info. calls . first ( ) . expect ( "single request call" ) . params ;
561530 assert_eq ! (
562531 params. get( "name" ) . map( String :: as_str) ,
@@ -659,6 +628,19 @@ mod tests {
659628 ) ;
660629 }
661630
631+ #[ test]
632+ fn accepts_any_valid_jsonrpc_params_shape ( ) {
633+ let body =
634+ br#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":["ignored",{"nested":true}]}"# ;
635+ let info = parse_jsonrpc_body ( body, JsonRpcInspectionMode :: JsonRpc ) ;
636+
637+ assert ! ( info. error. is_none( ) ) ;
638+ assert_eq ! (
639+ info. calls. first( ) . map( |call| call. method. as_str( ) ) ,
640+ Some ( "tools/call" )
641+ ) ;
642+ }
643+
662644 #[ test]
663645 fn recognizes_streamable_http_get_receive_streams ( ) {
664646 let request = L7Request {
@@ -675,7 +657,6 @@ mod tests {
675657 assert ! ( info. receive_stream) ;
676658 assert ! ( info. error. is_none( ) ) ;
677659 assert ! ( info. calls. is_empty( ) ) ;
678- assert ! ( info. params_sha256( ) . is_none( ) ) ;
679660 }
680661
681662 #[ test]
@@ -762,10 +743,6 @@ mod tests {
762743 assert_eq ! ( info. calls. len( ) , 2 ) ;
763744 assert_eq ! ( info. calls[ 0 ] . method, "tools/list" ) ;
764745 assert_eq ! ( info. calls[ 1 ] . method, "tools/call" ) ;
765- assert_eq ! (
766- info. calls[ 1 ] . params. get( "name" ) . map( String :: as_str) ,
767- Some ( "read_status" )
768- ) ;
769746 }
770747
771748 #[ test]
@@ -831,52 +808,4 @@ mod tests {
831808 Some ( "batch item invalid: JSON-RPC message includes both method and result/error" )
832809 ) ;
833810 }
834-
835- #[ test]
836- fn params_digest_is_canonical_and_redacted ( ) {
837- let first = parse_jsonrpc_body (
838- br#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"submit_report","arguments":{"scope":"workspace/main"}}}"# ,
839- JsonRpcInspectionMode :: JsonRpc ,
840- ) ;
841- let reordered = parse_jsonrpc_body (
842- br#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"arguments":{"scope":"workspace/main"},"name":"submit_report"}}"# ,
843- JsonRpcInspectionMode :: JsonRpc ,
844- ) ;
845- let changed = parse_jsonrpc_body (
846- br#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"submit_report","arguments":{"scope":"workspace/other"}}}"# ,
847- JsonRpcInspectionMode :: JsonRpc ,
848- ) ;
849-
850- let digest = first. params_sha256 ( ) . expect ( "params digest" ) ;
851- assert_eq ! ( Some ( digest. as_str( ) ) , reordered. params_sha256( ) . as_deref( ) ) ;
852- assert_ne ! ( Some ( digest. as_str( ) ) , changed. params_sha256( ) . as_deref( ) ) ;
853- assert_eq ! ( digest. len( ) , 64 ) ;
854- assert ! ( digest. chars( ) . all( |c| c. is_ascii_hexdigit( ) ) ) ;
855- assert ! ( !digest. contains( "workspace/main" ) ) ;
856- assert ! ( !digest. contains( "submit_report" ) ) ;
857- }
858-
859- #[ test]
860- fn batch_params_digest_covers_call_params_without_raw_values ( ) {
861- let batch = parse_jsonrpc_body (
862- br#"[
863- {"jsonrpc":"2.0","id":1,"method":"tools/list"},
864- {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"blocked_action"}}
865- ]"# ,
866- JsonRpcInspectionMode :: JsonRpc ,
867- ) ;
868- let empty_batch = parse_jsonrpc_body (
869- br#"[
870- {"jsonrpc":"2.0","id":1,"method":"tools/list"},
871- {"jsonrpc":"2.0","id":2,"method":"initialize"}
872- ]"# ,
873- JsonRpcInspectionMode :: JsonRpc ,
874- ) ;
875-
876- let digest = batch. params_sha256 ( ) . expect ( "batch params digest" ) ;
877- assert_eq ! ( digest. len( ) , 64 ) ;
878- assert ! ( digest. chars( ) . all( |c| c. is_ascii_hexdigit( ) ) ) ;
879- assert ! ( !digest. contains( "blocked_action" ) ) ;
880- assert ! ( empty_batch. params_sha256( ) . is_none( ) ) ;
881- }
882811}
0 commit comments