-
Notifications
You must be signed in to change notification settings - Fork 262
Add support for Tool.outputSchema
and CallToolResult.structuredContent
#316
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c2f65cc
5d24acc
efdb55f
c3a9ba7
6cad6c5
366d0af
b16fd38
d82056f
b174b63
cb28342
1b03666
3ed064d
70bf2b1
43a72da
4001d65
cff51c4
33b4d59
1274857
a1ef39e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,39 @@ | ||
//! Tool handler traits and types for MCP servers. | ||
//! | ||
//! This module provides the infrastructure for implementing tools that can be called | ||
//! by MCP clients. Tools can return either unstructured content (text, images) or | ||
//! structured JSON data with schemas. | ||
//! | ||
//! # Structured Output | ||
//! | ||
//! Tools can return structured JSON data using the [`Json`] wrapper type. | ||
//! When using `Json<T>`, the framework will: | ||
//! - Automatically generate a JSON schema for the output type | ||
//! - Validate the output against the schema | ||
//! - Return the data in the `structured_content` field of [`CallToolResult`] | ||
//! | ||
//! # Example | ||
//! | ||
//! ```rust,ignore | ||
//! use rmcp::{tool, Json}; | ||
//! use schemars::JsonSchema; | ||
//! use serde::{Serialize, Deserialize}; | ||
//! | ||
//! #[derive(Serialize, Deserialize, JsonSchema)] | ||
//! struct AnalysisResult { | ||
//! score: f64, | ||
//! summary: String, | ||
//! } | ||
//! | ||
//! #[tool(name = "analyze")] | ||
//! async fn analyze(&self, text: String) -> Result<Json<AnalysisResult>, String> { | ||
//! Ok(Json(AnalysisResult { | ||
//! score: 0.95, | ||
//! summary: "Positive sentiment".to_string(), | ||
//! })) | ||
//! } | ||
//! ``` | ||
|
||
use std::{ | ||
any::TypeId, borrow::Cow, collections::HashMap, future::Ready, marker::PhantomData, sync::Arc, | ||
}; | ||
|
@@ -10,6 +46,7 @@ use tokio_util::sync::CancellationToken; | |
pub use super::router::tool::{ToolRoute, ToolRouter}; | ||
use crate::{ | ||
RoleServer, | ||
handler::server::wrapper::Json, | ||
model::{CallToolRequestParam, CallToolResult, IntoContents, JsonObject}, | ||
schemars::generate::SchemaSettings, | ||
service::RequestContext, | ||
|
@@ -30,6 +67,43 @@ pub fn schema_for_type<T: JsonSchema>() -> JsonObject { | |
} | ||
} | ||
|
||
/// Validate that a JSON value conforms to basic type constraints from a schema. | ||
/// | ||
/// Note: This is a basic validation that only checks type compatibility. | ||
/// For full JSON Schema validation, a dedicated validation library would be needed. | ||
pub fn validate_against_schema( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we extract it like this ,For better scalability, And there seems to be some repetition here, How about this
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jokemanfire I'm on it! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jokemanfire done in a1ef39e |
||
value: &serde_json::Value, | ||
schema: &JsonObject, | ||
) -> Result<(), crate::ErrorData> { | ||
// Basic type validation | ||
if let Some(schema_type) = schema.get("type").and_then(|t| t.as_str()) { | ||
let value_type = get_json_value_type(value); | ||
|
||
if schema_type != value_type { | ||
return Err(crate::ErrorData::invalid_params( | ||
format!( | ||
"Value type does not match schema. Expected '{}', got '{}'", | ||
schema_type, value_type | ||
), | ||
None, | ||
)); | ||
} | ||
} | ||
|
||
Ok(()) | ||
} | ||
|
||
fn get_json_value_type(value: &serde_json::Value) -> &'static str { | ||
match value { | ||
serde_json::Value::Null => "null", | ||
serde_json::Value::Bool(_) => "boolean", | ||
serde_json::Value::Number(_) => "number", | ||
serde_json::Value::String(_) => "string", | ||
serde_json::Value::Array(_) => "array", | ||
serde_json::Value::Object(_) => "object", | ||
} | ||
} | ||
|
||
/// Call [`schema_for_type`] with a cache | ||
pub fn cached_schema_for_type<T: JsonSchema + std::any::Any>() -> Arc<JsonObject> { | ||
thread_local! { | ||
|
@@ -97,8 +171,26 @@ pub trait FromToolCallContextPart<S>: Sized { | |
) -> Result<Self, crate::ErrorData>; | ||
} | ||
|
||
/// Trait for converting tool return values into [`CallToolResult`]. | ||
/// | ||
/// This trait is automatically implemented for: | ||
/// - Types implementing [`IntoContents`] (returns unstructured content) | ||
/// - `Result<T, E>` where both `T` and `E` implement [`IntoContents`] | ||
/// - [`Json<T>`](crate::handler::server::wrapper::Json) where `T` implements [`Serialize`] (returns structured content) | ||
/// - `Result<Json<T>, E>` for structured results with errors | ||
/// | ||
/// The `#[tool]` macro uses this trait to convert tool function return values | ||
/// into the appropriate [`CallToolResult`] format. | ||
pub trait IntoCallToolResult { | ||
fn into_call_tool_result(self) -> Result<CallToolResult, crate::ErrorData>; | ||
|
||
/// Returns the output schema for this type, if any. | ||
/// | ||
/// This is used by the macro to automatically generate output schemas | ||
/// for tool functions that return structured data. | ||
fn output_schema() -> Option<Arc<JsonObject>> { | ||
None | ||
} | ||
} | ||
|
||
impl<T: IntoContents> IntoCallToolResult for T { | ||
|
@@ -125,6 +217,40 @@ impl<T: IntoCallToolResult> IntoCallToolResult for Result<T, crate::ErrorData> { | |
} | ||
} | ||
|
||
// Implementation for Json<T> to create structured content | ||
impl<T: Serialize + JsonSchema + 'static> IntoCallToolResult for Json<T> { | ||
fn into_call_tool_result(self) -> Result<CallToolResult, crate::ErrorData> { | ||
let value = serde_json::to_value(self.0).map_err(|e| { | ||
crate::ErrorData::internal_error( | ||
format!("Failed to serialize structured content: {}", e), | ||
None, | ||
) | ||
})?; | ||
|
||
Ok(CallToolResult::structured(value)) | ||
} | ||
|
||
fn output_schema() -> Option<Arc<JsonObject>> { | ||
Some(cached_schema_for_type::<T>()) | ||
} | ||
} | ||
|
||
// Implementation for Result<Json<T>, E> | ||
impl<T: Serialize + JsonSchema + 'static, E: IntoContents> IntoCallToolResult | ||
for Result<Json<T>, E> | ||
{ | ||
fn into_call_tool_result(self) -> Result<CallToolResult, crate::ErrorData> { | ||
match self { | ||
Ok(value) => value.into_call_tool_result(), | ||
Err(error) => Ok(CallToolResult::error(error.into_contents())), | ||
} | ||
} | ||
|
||
fn output_schema() -> Option<Arc<JsonObject>> { | ||
Json::<T>::output_schema() | ||
} | ||
} | ||
|
||
pin_project_lite::pin_project! { | ||
#[project = IntoCallToolResultFutProj] | ||
pub enum IntoCallToolResultFut<F, R> { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,28 +1,22 @@ | ||
use serde::Serialize; | ||
use std::borrow::Cow; | ||
|
||
use crate::model::IntoContents; | ||
use schemars::JsonSchema; | ||
|
||
/// Json wrapper | ||
/// Json wrapper for structured output | ||
/// | ||
/// This is used to tell the SDK to serialize the inner value into json | ||
/// When used with tools, this wrapper indicates that the value should be | ||
/// serialized as structured JSON content with an associated schema. | ||
/// The framework will place the JSON in the `structured_content` field | ||
/// of the tool result rather than the regular `content` field. | ||
pub struct Json<T>(pub T); | ||
|
||
impl<T> IntoContents for Json<T> | ||
where | ||
T: Serialize, | ||
{ | ||
fn into_contents(self) -> Vec<crate::model::Content> { | ||
let result = crate::model::Content::json(self.0); | ||
debug_assert!( | ||
result.is_ok(), | ||
"Json wrapped content should be able to serialized into json" | ||
); | ||
match result { | ||
Ok(content) => vec![content], | ||
Err(e) => { | ||
tracing::error!("failed to convert json content: {e}"); | ||
vec![] | ||
} | ||
} | ||
// Implement JsonSchema for Json<T> to delegate to T's schema | ||
impl<T: JsonSchema> JsonSchema for Json<T> { | ||
fn schema_name() -> Cow<'static, str> { | ||
T::schema_name() | ||
} | ||
|
||
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { | ||
T::json_schema(generator) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the complexity of the circle here a bit high?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Simplify this please.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@4t145 done in 70bf2b1