Skip to content

Commit 5b57d8a

Browse files
Brandon Bennettorca-ide
andcommitted
fix: address PR review - schema_for_output no longer validates or returns Result
- Add strip_output() that strips title/description without validating type (Dale #1) - Change schema_for_output to return Arc<JsonObject> instead of Result (Dale #2) - Cache only Arc<JsonObject> success values, not Result (Dale #3) - Remove dead unwrap_or_else panic paths in with_output_schema, ToolBase, and macros - Tighten test assertions from contains to assert_eq on type field (Dale #4) - Update test_schema_for_output_rejects_primitive to accept_primitive (SEP-2106) Co-authored-by: Orca <help@stably.ai>
1 parent 80a7479 commit 5b57d8a

5 files changed

Lines changed: 55 additions & 53 deletions

File tree

crates/rmcp-macros/src/tool.rs

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,6 @@ fn extract_schema_from_return_type(ret_type: &syn::Type) -> Option<Expr> {
2828
if let Some(inner_type) = extract_json_inner_type(ret_type) {
2929
return syn::parse2::<Expr>(quote! {
3030
rmcp::handler::server::tool::schema_for_output::<#inner_type>()
31-
.unwrap_or_else(|e| {
32-
panic!(
33-
"Invalid output schema for Json<{}>: {}",
34-
std::any::type_name::<#inner_type>(),
35-
e
36-
)
37-
})
3831
})
3932
.ok();
4033
}
@@ -65,13 +58,6 @@ fn extract_schema_from_return_type(ret_type: &syn::Type) -> Option<Expr> {
6558

6659
syn::parse2::<Expr>(quote! {
6760
rmcp::handler::server::tool::schema_for_output::<#inner_type>()
68-
.unwrap_or_else(|e| {
69-
panic!(
70-
"Invalid output schema for Result<Json<{}>, E>: {}",
71-
std::any::type_name::<#inner_type>(),
72-
e
73-
)
74-
})
7561
})
7662
.ok()
7763
}

crates/rmcp/src/handler/server/common.rs

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -106,32 +106,40 @@ pub fn schema_for_empty_input() -> Arc<JsonObject> {
106106
EMPTY.clone()
107107
}
108108

109-
/// Generate a JSON schema for outputSchema (must have root type "object"; top-level "title" and "description" are removed)
110-
pub fn schema_for_output<T: JsonSchema + std::any::Any>() -> Result<Arc<JsonObject>, String> {
109+
/// Strip top-level `title` and `description` from a JSON schema for outputSchema.
110+
/// Unlike `validate_and_strip`, this performs no validation — output schemas are not
111+
/// restricted to `type: "object"` (per SEP-2106).
112+
fn strip_output(raw: &Arc<JsonObject>) -> Arc<JsonObject> {
113+
let mut object = raw.as_ref().clone();
114+
object.remove("title");
115+
object.remove("description");
116+
Arc::new(object)
117+
}
118+
119+
/// Generate and strip a JSON schema for outputSchema (top-level "title" and
120+
/// "description" are removed; output schemas are not restricted to root type "object").
121+
pub fn schema_for_output<T: JsonSchema + std::any::Any>() -> Arc<JsonObject> {
111122
thread_local! {
112-
static CACHE_FOR_OUTPUT: std::sync::RwLock<HashMap<TypeId, Result<Arc<JsonObject>, String>>> = Default::default();
123+
static CACHE_FOR_OUTPUT: std::sync::RwLock<HashMap<TypeId, Arc<JsonObject>>> = Default::default();
113124
};
114125

115126
CACHE_FOR_OUTPUT.with(|cache| {
116-
// Try to get from cache first
117-
if let Some(result) = cache
127+
if let Some(schema) = cache
118128
.read()
119129
.expect("output schema cache lock poisoned")
120130
.get(&TypeId::of::<T>())
121131
{
122-
return result.clone();
132+
return schema.clone();
123133
}
124134

125-
// Generate, validate, and strip unnecessary top-level fields
126-
let result = validate_and_strip(&schema_for_type::<T>(), "outputSchema");
135+
let schema = strip_output(&schema_for_type::<T>());
127136

128-
// Cache the result (both success and error cases)
129137
cache
130138
.write()
131139
.expect("output schema cache lock poisoned")
132-
.insert(TypeId::of::<T>(), result.clone());
140+
.insert(TypeId::of::<T>(), schema.clone());
133141

134-
result
142+
schema
135143
})
136144
}
137145

@@ -305,32 +313,52 @@ mod tests {
305313
assert!(Arc::ptr_eq(&schema, &cloned));
306314
}
307315

316+
#[test]
317+
fn test_schema_for_output_accepts_primitive() {
318+
let schema = schema_for_output::<i32>();
319+
assert_eq!(schema.get("type"), Some(&serde_json::json!("integer")));
320+
}
321+
322+
#[test]
323+
fn test_schema_for_output_accepts_object() {
324+
let schema = schema_for_output::<TestObject>();
325+
assert_eq!(schema.get("type"), Some(&serde_json::json!("object")));
326+
}
327+
328+
#[test]
329+
fn test_schema_for_output_strips_top_level_title() {
330+
let schema = schema_for_output::<TestObject>();
331+
assert!(!schema.contains_key("title"));
332+
}
333+
334+
#[test]
335+
fn test_schema_for_output_strips_top_level_description() {
336+
let schema = schema_for_output::<TestObject>();
337+
assert!(!schema.contains_key("description"));
338+
}
339+
308340
#[rstest]
309-
#[case::output(schema_for_output::<i32>)]
310341
#[case::input(schema_for_input::<i32>)]
311-
fn test_schema_for_object_wrappers_reject_primitives(
342+
fn test_schema_for_input_rejects_primitives(
312343
#[case] schema_fn: fn() -> Result<Arc<JsonObject>, String>,
313344
) {
314345
let result = schema_fn();
315346
assert!(result.is_err());
316347
}
317348

318349
#[rstest]
319-
#[case::output(schema_for_output::<TestObject>)]
320350
#[case::input(schema_for_input::<TestObject>)]
321-
fn test_schema_for_object_wrappers_accept_objects(
351+
fn test_schema_for_input_accepts_objects(
322352
#[case] schema_fn: fn() -> Result<Arc<JsonObject>, String>,
323353
) {
324354
let result = schema_fn();
325355
assert!(result.is_ok());
326356
}
327357

328358
#[rstest]
329-
#[case::output_title(schema_for_output::<TestObject>, "title")]
330-
#[case::output_description(schema_for_output::<TestObject>, "description")]
331359
#[case::input_title(schema_for_input::<TestObject>, "title")]
332360
#[case::input_description(schema_for_input::<TestObject>, "description")]
333-
fn test_schema_for_object_wrappers_strip_top_level_metadata(
361+
fn test_schema_for_input_strips_top_level_metadata(
334362
#[case] schema_fn: fn() -> Result<Arc<JsonObject>, String>,
335363
#[case] field: &str,
336364
) {

crates/rmcp/src/handler/server/router/tool/tool_traits.rs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,7 @@ pub trait ToolBase {
6565
///
6666
/// If the tool does not have any output, you should override this methods to return [`None`].
6767
fn output_schema() -> Option<Arc<JsonObject>> {
68-
Some(schema_for_output::<Self::Output>().unwrap_or_else(|e| {
69-
panic!(
70-
"Invalid output schema for ToolBase::Output type `{0}`: {1}",
71-
std::any::type_name::<Self::Output>(),
72-
e,
73-
);
74-
}))
68+
Some(schema_for_output::<Self::Output>())
7569
}
7670

7771
fn annotations() -> Option<ToolAnnotations> {

crates/rmcp/src/model/tool.rs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -315,15 +315,9 @@ impl Tool {
315315
}
316316

317317
/// Set the output schema using a type that implements JsonSchema
318-
///
319-
/// # Panics
320-
///
321-
/// Panics if the generated schema does not have root type "object" as required by MCP specification.
322318
#[cfg(feature = "server")]
323319
pub fn with_output_schema<T: JsonSchema + 'static>(mut self) -> Self {
324-
let schema = crate::handler::server::tool::schema_for_output::<T>()
325-
.unwrap_or_else(|e| panic!("Invalid output schema for tool '{}': {}", self.name, e));
326-
self.output_schema = Some(schema);
320+
self.output_schema = Some(crate::handler::server::tool::schema_for_output::<T>());
327321
self
328322
}
329323

crates/rmcp/tests/test_tool_builder_methods.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,8 @@ fn test_with_output_schema() {
2222

2323
assert!(tool.output_schema.is_some());
2424

25-
// Verify the schema contains expected fields
26-
let schema_str = serde_json::to_string(tool.output_schema.as_ref().unwrap()).unwrap();
27-
assert!(schema_str.contains("greeting"));
28-
assert!(schema_str.contains("is_adult"));
25+
let schema = tool.output_schema.as_ref().unwrap();
26+
assert_eq!(schema.get("type"), Some(&serde_json::json!("object")));
2927
}
3028

3129
#[test]
@@ -57,7 +55,9 @@ fn test_chained_builder_methods() {
5755
assert!(input_schema_str.contains("name"));
5856
assert!(input_schema_str.contains("age"));
5957

60-
let output_schema_str = serde_json::to_string(tool.output_schema.as_ref().unwrap()).unwrap();
61-
assert!(output_schema_str.contains("greeting"));
62-
assert!(output_schema_str.contains("is_adult"));
58+
let output_schema = tool.output_schema.as_ref().unwrap();
59+
assert_eq!(
60+
output_schema.get("type"),
61+
Some(&serde_json::json!("object"))
62+
);
6363
}

0 commit comments

Comments
 (0)