@@ -90,12 +90,15 @@ pub fn parse_jsonrpc_body(body: &[u8]) -> JsonRpcRequestInfo {
9090 }
9191 let mut calls = Vec :: new ( ) ;
9292 for item in & items {
93- let Some ( call) = parse_jsonrpc_call ( item) else {
94- return JsonRpcRequestInfo {
95- calls : Vec :: new ( ) ,
96- is_batch : true ,
97- error : Some ( "batch item missing or non-string 'method' field" . to_string ( ) ) ,
98- } ;
93+ let call = match parse_jsonrpc_call ( item) {
94+ Ok ( call) => call,
95+ Err ( error) => {
96+ return JsonRpcRequestInfo {
97+ calls : Vec :: new ( ) ,
98+ is_batch : true ,
99+ error : Some ( format ! ( "batch item invalid: {error}" ) ) ,
100+ } ;
101+ }
99102 } ;
100103 calls. push ( call) ;
101104 }
@@ -106,12 +109,15 @@ pub fn parse_jsonrpc_body(body: &[u8]) -> JsonRpcRequestInfo {
106109 } ;
107110 }
108111
109- let Some ( call) = parse_jsonrpc_call ( & value) else {
110- return JsonRpcRequestInfo {
111- calls : Vec :: new ( ) ,
112- is_batch : false ,
113- error : Some ( "missing or non-string 'method' field" . to_string ( ) ) ,
114- } ;
112+ let call = match parse_jsonrpc_call ( & value) {
113+ Ok ( call) => call,
114+ Err ( error) => {
115+ return JsonRpcRequestInfo {
116+ calls : Vec :: new ( ) ,
117+ is_batch : false ,
118+ error : Some ( error) ,
119+ } ;
120+ }
115121 } ;
116122 JsonRpcRequestInfo {
117123 calls : vec ! [ call] ,
@@ -120,20 +126,33 @@ pub fn parse_jsonrpc_body(body: &[u8]) -> JsonRpcRequestInfo {
120126 }
121127}
122128
123- fn parse_jsonrpc_call ( value : & serde_json:: Value ) -> Option < JsonRpcCallInfo > {
124- let method = value. get ( "method" ) . and_then ( |m| m. as_str ( ) ) ?;
125- Some ( JsonRpcCallInfo {
129+ fn parse_jsonrpc_call ( value : & serde_json:: Value ) -> std:: result:: Result < JsonRpcCallInfo , String > {
130+ let version = value
131+ . get ( "jsonrpc" )
132+ . and_then ( |v| v. as_str ( ) )
133+ . ok_or_else ( || "missing or non-string 'jsonrpc' field" . to_string ( ) ) ?;
134+ if version != "2.0" {
135+ return Err ( format ! ( "unsupported JSON-RPC version '{version}'" ) ) ;
136+ }
137+ let method = value
138+ . get ( "method" )
139+ . and_then ( |m| m. as_str ( ) )
140+ . ok_or_else ( || "missing or non-string 'method' field" . to_string ( ) ) ?;
141+ let params = value
142+ . get ( "params" )
143+ . map_or_else ( || Ok ( HashMap :: new ( ) ) , flatten_jsonrpc_params) ?;
144+ Ok ( JsonRpcCallInfo {
126145 method : method. to_string ( ) ,
127- params : value
128- . get ( "params" )
129- . map_or_else ( HashMap :: new, flatten_jsonrpc_params) ,
146+ params,
130147 } )
131148}
132149
133- fn flatten_jsonrpc_params ( value : & serde_json:: Value ) -> HashMap < String , String > {
150+ fn flatten_jsonrpc_params (
151+ value : & serde_json:: Value ,
152+ ) -> std:: result:: Result < HashMap < String , String > , String > {
134153 let mut params = HashMap :: new ( ) ;
135- flatten_json_value ( "" , value, & mut params) ;
136- params
154+ flatten_json_value ( "" , value, & mut params) ? ;
155+ Ok ( params)
137156}
138157
139158fn canonical_params_map ( params : & HashMap < String , String > ) -> BTreeMap < String , String > {
@@ -148,29 +167,50 @@ fn sha256_json(value: &impl serde::Serialize) -> String {
148167 hex:: encode ( Sha256 :: digest ( & encoded) )
149168}
150169
151- fn flatten_json_value ( prefix : & str , value : & serde_json:: Value , out : & mut HashMap < String , String > ) {
170+ fn flatten_json_value (
171+ prefix : & str ,
172+ value : & serde_json:: Value ,
173+ out : & mut HashMap < String , String > ,
174+ ) -> std:: result:: Result < ( ) , String > {
152175 match value {
153176 serde_json:: Value :: Object ( map) => {
154177 for ( key, child) in map {
178+ if key. contains ( '.' ) {
179+ return Err ( format ! (
180+ "ambiguous dotted params key '{key}' is not allowed"
181+ ) ) ;
182+ }
155183 let next = if prefix. is_empty ( ) {
156184 key. clone ( )
157185 } else {
158186 format ! ( "{prefix}.{key}" )
159187 } ;
160- flatten_json_value ( & next, child, out) ;
188+ flatten_json_value ( & next, child, out) ? ;
161189 }
162190 }
163191 serde_json:: Value :: String ( s) if !prefix. is_empty ( ) => {
164- out . insert ( prefix. to_string ( ) , s. clone ( ) ) ;
192+ insert_flattened_param ( out , prefix, s. clone ( ) ) ? ;
165193 }
166194 serde_json:: Value :: Number ( n) if !prefix. is_empty ( ) => {
167- out . insert ( prefix. to_string ( ) , n. to_string ( ) ) ;
195+ insert_flattened_param ( out , prefix, n. to_string ( ) ) ? ;
168196 }
169197 serde_json:: Value :: Bool ( b) if !prefix. is_empty ( ) => {
170- out . insert ( prefix. to_string ( ) , b. to_string ( ) ) ;
198+ insert_flattened_param ( out , prefix, b. to_string ( ) ) ? ;
171199 }
172200 _ => { }
173201 }
202+ Ok ( ( ) )
203+ }
204+
205+ fn insert_flattened_param (
206+ out : & mut HashMap < String , String > ,
207+ key : & str ,
208+ value : String ,
209+ ) -> std:: result:: Result < ( ) , String > {
210+ if out. insert ( key. to_string ( ) , value) . is_some ( ) {
211+ return Err ( format ! ( "ambiguous params key collision at '{key}'" ) ) ;
212+ }
213+ Ok ( ( ) )
174214}
175215
176216#[ cfg( test) ]
@@ -205,6 +245,70 @@ mod tests {
205245 ) ;
206246 }
207247
248+ #[ test]
249+ fn rejects_literal_dotted_param_keys ( ) {
250+ let body = br#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"arguments.scope":"workspace/other","arguments":{"scope":"workspace/main"}}}"# ;
251+ let info = parse_jsonrpc_body ( body) ;
252+
253+ assert ! ( info. calls. is_empty( ) ) ;
254+ assert ! (
255+ info. error
256+ . as_deref( )
257+ . is_some_and( |error| error. contains( "ambiguous dotted params key" ) ) ,
258+ "expected dotted params key error, got {info:?}"
259+ ) ;
260+ }
261+
262+ #[ test]
263+ fn rejects_requests_missing_jsonrpc_version ( ) {
264+ let body = br#"{"id":1,"method":"tools/list"}"# ;
265+ let info = parse_jsonrpc_body ( body) ;
266+
267+ assert ! ( info. calls. is_empty( ) ) ;
268+ assert_eq ! (
269+ info. error. as_deref( ) ,
270+ Some ( "missing or non-string 'jsonrpc' field" )
271+ ) ;
272+ }
273+
274+ #[ test]
275+ fn rejects_batch_items_missing_jsonrpc_version ( ) {
276+ let body = br#"[
277+ {"jsonrpc":"2.0","id":1,"method":"tools/list"},
278+ {"id":2,"method":"tools/call","params":{"name":"read_status"}}
279+ ]"# ;
280+ let info = parse_jsonrpc_body ( body) ;
281+
282+ assert ! ( info. calls. is_empty( ) ) ;
283+ assert ! ( info. is_batch) ;
284+ assert_eq ! (
285+ info. error. as_deref( ) ,
286+ Some ( "batch item invalid: missing or non-string 'jsonrpc' field" )
287+ ) ;
288+ }
289+
290+ #[ test]
291+ fn rejects_unsupported_jsonrpc_version ( ) {
292+ let body = br#"{"jsonrpc":"1.0","id":1,"method":"tools/list"}"# ;
293+ let info = parse_jsonrpc_body ( body) ;
294+
295+ assert ! ( info. calls. is_empty( ) ) ;
296+ assert_eq ! (
297+ info. error. as_deref( ) ,
298+ Some ( "unsupported JSON-RPC version '1.0'" )
299+ ) ;
300+ }
301+
302+ #[ test]
303+ fn detects_flattened_param_collisions ( ) {
304+ let mut params = HashMap :: from ( [ ( "arguments.scope" . to_string ( ) , "first" . to_string ( ) ) ] ) ;
305+
306+ let error = insert_flattened_param ( & mut params, "arguments.scope" , "second" . to_string ( ) )
307+ . expect_err ( "duplicate flattened key should be ambiguous" ) ;
308+
309+ assert ! ( error. contains( "ambiguous params key collision" ) ) ;
310+ }
311+
208312 #[ test]
209313 fn parses_valid_batch_without_error ( ) {
210314 let body = br#"[
0 commit comments