1
- use std:: collections:: HashMap ;
1
+ use std:: collections:: { HashMap , HashSet } ;
2
2
3
3
use serde_json:: { Map , Value } ;
4
4
@@ -7,8 +7,71 @@ use crate::{
7
7
types:: { McpToolSParams , ParamTypes } ,
8
8
} ;
9
9
10
+ /// Resolves a $ref path to its target value in the schema.
11
+ fn resolve_ref < ' a > (
12
+ ref_path : & str ,
13
+ root_schema : & ' a Value ,
14
+ visited : & mut HashSet < String > ,
15
+ ) -> DiscoveryResult < & ' a Value > {
16
+ if !ref_path. starts_with ( "#/" ) {
17
+ return Err ( DiscoveryError :: InvalidSchema ( format ! (
18
+ "$ref '{}' must start with '#/'" ,
19
+ ref_path
20
+ ) ) ) ;
21
+ }
22
+
23
+ if !visited. insert ( ref_path. to_string ( ) ) {
24
+ return Err ( DiscoveryError :: InvalidSchema ( format ! (
25
+ "Cycle detected in $ref path '{}'" ,
26
+ ref_path
27
+ ) ) ) ;
28
+ }
29
+
30
+ let path = ref_path. trim_start_matches ( "#/" ) . split ( '/' ) ;
31
+ let mut current = root_schema;
32
+
33
+ for segment in path {
34
+ if segment. is_empty ( ) {
35
+ return Err ( DiscoveryError :: InvalidSchema ( format ! (
36
+ "Invalid $ref path '{}': empty segment" ,
37
+ ref_path
38
+ ) ) ) ;
39
+ }
40
+ current = match current {
41
+ Value :: Object ( obj) => obj. get ( segment) . ok_or_else ( || {
42
+ DiscoveryError :: InvalidSchema ( format ! (
43
+ "Invalid $ref path '{}': segment '{}' not found" ,
44
+ ref_path, segment
45
+ ) )
46
+ } ) ?,
47
+ Value :: Array ( arr) => segment
48
+ . parse :: < usize > ( )
49
+ . ok ( )
50
+ . and_then ( |i| arr. get ( i) )
51
+ . ok_or_else ( || {
52
+ DiscoveryError :: InvalidSchema ( format ! (
53
+ "Invalid $ref path '{}': segment '{}' not found in array" ,
54
+ ref_path, segment
55
+ ) )
56
+ } ) ?,
57
+ _ => {
58
+ return Err ( DiscoveryError :: InvalidSchema ( format ! (
59
+ "Invalid $ref path '{}': cannot traverse into non-object/array" ,
60
+ ref_path
61
+ ) ) )
62
+ }
63
+ } ;
64
+ }
65
+
66
+ Ok ( current)
67
+ }
68
+
10
69
/// Parses an object schema into a vector of `McpToolSParams`.
11
- pub fn param_object ( object_map : & Map < String , Value > ) -> DiscoveryResult < Vec < McpToolSParams > > {
70
+ pub fn param_object (
71
+ object_map : & Map < String , Value > ,
72
+ root_schema : & Value ,
73
+ visited : & mut HashSet < String > ,
74
+ ) -> DiscoveryResult < Vec < McpToolSParams > > {
12
75
let properties = object_map
13
76
. get ( "properties" )
14
77
. and_then ( |v| v. as_object ( ) )
@@ -31,7 +94,7 @@ pub fn param_object(object_map: &Map<String, Value>) -> DiscoveryResult<Vec<McpT
31
94
"Property '{}' is not an object" ,
32
95
param_name
33
96
) ) ) ?;
34
- let param_type = param_type ( param_value) ?;
97
+ let param_type = param_type ( param_value, root_schema , visited ) ?;
35
98
let param_description = object_map
36
99
. get ( "description" )
37
100
. and_then ( |v| v. as_str ( ) )
@@ -50,7 +113,26 @@ pub fn param_object(object_map: &Map<String, Value>) -> DiscoveryResult<Vec<McpT
50
113
}
51
114
52
115
/// Determines the parameter type from a schema definition.
53
- pub fn param_type ( type_info : & Map < String , Value > ) -> DiscoveryResult < ParamTypes > {
116
+ pub fn param_type (
117
+ type_info : & Map < String , Value > ,
118
+ root_schema : & Value ,
119
+ visited : & mut HashSet < String > ,
120
+ ) -> DiscoveryResult < ParamTypes > {
121
+ // Handle $ref
122
+ if let Some ( ref_path) = type_info. get ( "$ref" ) {
123
+ let ref_path_str = ref_path. as_str ( ) . ok_or ( DiscoveryError :: InvalidSchema (
124
+ "$ref must be a string" . to_string ( ) ,
125
+ ) ) ?;
126
+ let ref_value = resolve_ref ( ref_path_str, root_schema, visited) ?;
127
+ let ref_map = ref_value
128
+ . as_object ( )
129
+ . ok_or ( DiscoveryError :: InvalidSchema ( format ! (
130
+ "$ref '{}' does not point to an object" ,
131
+ ref_path_str
132
+ ) ) ) ?;
133
+ return param_type ( ref_map, root_schema, visited) ;
134
+ }
135
+
54
136
// Check for 'enum' keyword
55
137
if let Some ( enum_values) = type_info. get ( "enum" ) {
56
138
let values = enum_values. as_array ( ) . ok_or ( DiscoveryError :: InvalidSchema (
@@ -90,6 +172,7 @@ pub fn param_type(type_info: &Map<String, Value>) -> DiscoveryResult<ParamTypes>
90
172
) ) ;
91
173
}
92
174
175
+ // Check for 'anyOf'
93
176
if let Some ( any_of) = type_info. get ( "anyOf" ) {
94
177
let any_of_array = any_of. as_array ( ) . ok_or ( DiscoveryError :: InvalidSchema (
95
178
"'anyOf' field must be an array" . to_string ( ) ,
@@ -104,7 +187,7 @@ pub fn param_type(type_info: &Map<String, Value>) -> DiscoveryResult<ParamTypes>
104
187
let item_map = item. as_object ( ) . ok_or ( DiscoveryError :: InvalidSchema (
105
188
"Items in 'anyOf' must be objects" . to_string ( ) ,
106
189
) ) ?;
107
- enum_types. push ( param_type ( item_map) ?) ;
190
+ enum_types. push ( param_type ( item_map, root_schema , visited ) ?) ;
108
191
}
109
192
return Ok ( ParamTypes :: Anyof ( enum_types) ) ;
110
193
}
@@ -124,7 +207,7 @@ pub fn param_type(type_info: &Map<String, Value>) -> DiscoveryResult<ParamTypes>
124
207
let item_map = item. as_object ( ) . ok_or ( DiscoveryError :: InvalidSchema (
125
208
"Items in 'oneOf' must be objects" . to_string ( ) ,
126
209
) ) ?;
127
- one_of_types. push ( param_type ( item_map) ?) ;
210
+ one_of_types. push ( param_type ( item_map, root_schema , visited ) ?) ;
128
211
}
129
212
return Ok ( ParamTypes :: OneOf ( one_of_types) ) ;
130
213
}
@@ -144,12 +227,12 @@ pub fn param_type(type_info: &Map<String, Value>) -> DiscoveryResult<ParamTypes>
144
227
let item_map = item. as_object ( ) . ok_or ( DiscoveryError :: InvalidSchema (
145
228
"Items in 'allOf' must be objects" . to_string ( ) ,
146
229
) ) ?;
147
- all_of_types. push ( param_type ( item_map) ?) ;
230
+ all_of_types. push ( param_type ( item_map, root_schema , visited ) ?) ;
148
231
}
149
232
return Ok ( ParamTypes :: AllOf ( all_of_types) ) ;
150
233
}
151
234
152
- // other types
235
+ // Other types
153
236
let type_name =
154
237
type_info
155
238
. get ( "type" )
@@ -166,22 +249,34 @@ pub fn param_type(type_info: &Map<String, Value>) -> DiscoveryResult<ParamTypes>
166
249
"Missing or invalid 'items' field in array type" . to_string ( ) ,
167
250
) ,
168
251
) ?;
169
- Ok ( ParamTypes :: Array ( vec ! [ param_type( items_map) ?] ) )
252
+ Ok ( ParamTypes :: Array ( vec ! [ param_type(
253
+ items_map,
254
+ root_schema,
255
+ visited,
256
+ ) ?] ) )
170
257
}
171
- "object" => Ok ( ParamTypes :: Object ( param_object ( type_info) ?) ) ,
258
+ "object" => Ok ( ParamTypes :: Object ( param_object (
259
+ type_info,
260
+ root_schema,
261
+ visited,
262
+ ) ?) ) ,
172
263
_ => Ok ( ParamTypes :: Primitive ( type_name. to_string ( ) ) ) ,
173
264
}
174
265
}
175
266
267
+ /// Processes tool parameters with a given properties map and root schema.
176
268
pub fn tool_params (
177
269
properties : & Option < HashMap < String , Map < String , Value > > > ,
270
+ root_schema : & Value ,
178
271
) -> Vec < McpToolSParams > {
272
+ let mut visited = HashSet :: new ( ) ;
179
273
let result = properties. clone ( ) . map ( |props| {
180
274
let mut params: Vec < _ > = props
181
275
. iter ( )
182
276
. map ( |( prop_name, prop_map) | {
183
277
let param_name = prop_name. to_owned ( ) ;
184
- let prop_type = param_type ( prop_map) . unwrap ( ) ;
278
+ let prop_type = param_type ( prop_map, root_schema, & mut visited)
279
+ . unwrap_or_else ( |_| ParamTypes :: Primitive ( "unknown" . to_string ( ) ) ) ;
185
280
let prop_description = prop_map
186
281
. get ( "description" )
187
282
. and_then ( |v| v. as_str ( ) )
0 commit comments