@@ -50,9 +50,9 @@ pub fn schema_for_type<T: JsonSchema + std::any::Any>() -> Arc<JsonObject> {
5050 } )
5151}
5252
53- /// Validate that the schema root is `type: "object"` (per MCP spec) and strip top-level
54- /// `title`/`description` (the wrapper type name and doc, which are noise to the LLM).
55- fn validate_and_strip ( raw : & Arc < JsonObject > , purpose : & str ) -> Result < Arc < JsonObject > , String > {
53+ /// Validate that the schema root is `type: "object"` (per MCP spec for inputSchema) and
54+ /// strip top-level `title`/`description` (the wrapper type name and doc, which are noise to the LLM).
55+ fn validate_and_strip_input ( raw : & Arc < JsonObject > ) -> Result < Arc < JsonObject > , String > {
5656 match raw. get ( "type" ) {
5757 Some ( serde_json:: Value :: String ( t) ) if t == "object" => {
5858 let mut object = raw. as_ref ( ) . clone ( ) ;
@@ -61,17 +61,27 @@ fn validate_and_strip(raw: &Arc<JsonObject>, purpose: &str) -> Result<Arc<JsonOb
6161 Ok ( Arc :: new ( object) )
6262 }
6363 Some ( serde_json:: Value :: String ( t) ) => Err ( format ! (
64- "MCP specification requires tool {purpose} to have root type 'object', but found '{t}'."
65- ) ) ,
66- None => Err ( format ! (
67- "Schema is missing 'type' field. MCP specification requires {purpose} to have root type 'object'."
64+ "MCP specification requires tool inputSchema to have root type 'object', but found '{t}'."
6865 ) ) ,
66+ None => Err (
67+ "Schema is missing 'type' field. MCP specification requires inputSchema to have root type 'object'." . to_string ( )
68+ ) ,
6969 Some ( other) => Err ( format ! (
7070 "Schema 'type' field has unexpected format: {other:?}. Expected \" object\" ."
7171 ) ) ,
7272 }
7373}
7474
75+ /// Strip top-level `title`/`description` from a JSON schema for outputSchema.
76+ /// Unlike inputSchema, outputSchema may have any JSON Schema 2020-12 root type
77+ /// (objects, arrays, primitives, compositions) per SEP-2106.
78+ fn validate_and_strip_output ( raw : & Arc < JsonObject > ) -> Arc < JsonObject > {
79+ let mut object = raw. as_ref ( ) . clone ( ) ;
80+ object. remove ( "title" ) ;
81+ object. remove ( "description" ) ;
82+ Arc :: new ( object)
83+ }
84+
7585/// Generate, validate, and strip a JSON schema for inputSchema (must have root type "object";
7686/// top-level "title" and "description" are removed).
7787pub fn schema_for_input < T : JsonSchema + std:: any:: Any > ( ) -> Result < Arc < JsonObject > , String > {
@@ -86,7 +96,7 @@ pub fn schema_for_input<T: JsonSchema + std::any::Any>() -> Result<Arc<JsonObjec
8696 {
8797 return result. clone ( ) ;
8898 }
89- let result = validate_and_strip ( & schema_for_type :: < T > ( ) , "inputSchema" ) ;
99+ let result = validate_and_strip_input ( & schema_for_type :: < T > ( ) ) ;
90100 cache
91101 . write ( )
92102 . expect ( "input schema cache lock poisoned" )
@@ -106,7 +116,10 @@ pub fn schema_for_empty_input() -> Arc<JsonObject> {
106116 EMPTY . clone ( )
107117}
108118
109- /// Generate a JSON schema for outputSchema (must have root type "object"; top-level "title" and "description" are removed)
119+ /// Generate and strip a JSON schema for outputSchema.
120+ /// Unlike inputSchema, outputSchema accepts any JSON Schema 2020-12 root type
121+ /// (objects, arrays, primitives, compositions) per SEP-2106.
122+ /// Top-level "title" and "description" are always removed.
110123pub fn schema_for_output < T : JsonSchema + std:: any:: Any > ( ) -> Result < Arc < JsonObject > , String > {
111124 thread_local ! {
112125 static CACHE_FOR_OUTPUT : std:: sync:: RwLock <HashMap <TypeId , Result <Arc <JsonObject >, String >>> = Default :: default ( ) ;
@@ -122,10 +135,8 @@ pub fn schema_for_output<T: JsonSchema + std::any::Any>() -> Result<Arc<JsonObje
122135 return result. clone ( ) ;
123136 }
124137
125- // Generate, validate, and strip unnecessary top-level fields
126- let result = validate_and_strip ( & schema_for_type :: < T > ( ) , "outputSchema" ) ;
138+ let result = Ok ( validate_and_strip_output ( & schema_for_type :: < T > ( ) ) ) ;
127139
128- // Cache the result (both success and error cases)
129140 cache
130141 . write ( )
131142 . expect ( "output schema cache lock poisoned" )
@@ -306,9 +317,24 @@ mod tests {
306317 }
307318
308319 #[ test]
309- fn test_schema_for_output_rejects_primitive ( ) {
320+ fn test_schema_for_output_accepts_primitive ( ) {
310321 let result = schema_for_output :: < i32 > ( ) ;
311- assert ! ( result. is_err( ) , ) ;
322+ assert ! ( result. is_ok( ) ) ;
323+ }
324+
325+ #[ test]
326+ fn test_schema_for_output_accepts_array ( ) {
327+ let result = schema_for_output :: < Vec < i32 > > ( ) ;
328+ assert ! ( result. is_ok( ) ) ;
329+ let schema = result. unwrap ( ) ;
330+ assert_eq ! ( schema. get( "type" ) , Some ( & serde_json:: json!( "array" ) ) ) ;
331+ assert ! ( schema. contains_key( "items" ) ) ;
332+ }
333+
334+ #[ test]
335+ fn test_schema_for_output_strips_title_for_primitive ( ) {
336+ let schema = schema_for_output :: < i32 > ( ) . unwrap ( ) ;
337+ assert ! ( !schema. contains_key( "title" ) ) ;
312338 }
313339
314340 #[ test]
0 commit comments