diff --git a/crates/rmcp-macros/src/tool.rs b/crates/rmcp-macros/src/tool.rs index 2ff22289..78c7261c 100644 --- a/crates/rmcp-macros/src/tool.rs +++ b/crates/rmcp-macros/src/tool.rs @@ -10,6 +10,8 @@ pub struct ToolAttribute { pub description: Option, /// A JSON Schema object defining the expected parameters for the tool pub input_schema: Option, + /// An optional JSON Schema object defining the structure of the tool's output + pub output_schema: Option, /// Optional additional tool information. pub annotations: Option, } @@ -18,6 +20,7 @@ pub struct ResolvedToolAttribute { pub name: String, pub description: Option, pub input_schema: Expr, + pub output_schema: Option, pub annotations: Expr, } @@ -27,6 +30,7 @@ impl ResolvedToolAttribute { name, description, input_schema, + output_schema, annotations, } = self; let description = if let Some(description) = description { @@ -34,12 +38,18 @@ impl ResolvedToolAttribute { } else { quote! { None } }; + let output_schema = if let Some(output_schema) = output_schema { + quote! { Some(#output_schema) } + } else { + quote! { None } + }; let tokens = quote! { pub fn #fn_ident() -> rmcp::model::Tool { rmcp::model::Tool { name: #name.into(), description: #description, input_schema: #input_schema, + output_schema: #output_schema, annotations: #annotations, } } @@ -89,6 +99,63 @@ fn none_expr() -> Expr { syn::parse2::(quote! { None }).unwrap() } +/// Check if a type is Json and extract the inner type T +fn extract_json_inner_type(ty: &syn::Type) -> Option<&syn::Type> { + if let syn::Type::Path(type_path) = ty { + if let Some(last_segment) = type_path.path.segments.last() { + if last_segment.ident == "Json" { + if let syn::PathArguments::AngleBracketed(args) = &last_segment.arguments { + if let Some(syn::GenericArgument::Type(inner_type)) = args.args.first() { + return Some(inner_type); + } + } + } + } + } + None +} + +/// Extract schema expression from a function's return type +/// Handles patterns like Json and Result, E> +fn extract_schema_from_return_type(ret_type: &syn::Type) -> Option { + // First, try direct Json + if let Some(inner_type) = extract_json_inner_type(ret_type) { + return syn::parse2::(quote! { + rmcp::handler::server::tool::cached_schema_for_type::<#inner_type>() + }) + .ok(); + } + + // Then, try Result, E> + let type_path = match ret_type { + syn::Type::Path(path) => path, + _ => return None, + }; + + let last_segment = type_path.path.segments.last()?; + + if last_segment.ident != "Result" { + return None; + } + + let args = match &last_segment.arguments { + syn::PathArguments::AngleBracketed(args) => args, + _ => return None, + }; + + let ok_type = match args.args.first()? { + syn::GenericArgument::Type(ty) => ty, + _ => return None, + }; + + let inner_type = extract_json_inner_type(ok_type)?; + + syn::parse2::(quote! { + rmcp::handler::server::tool::cached_schema_for_type::<#inner_type>() + }) + .ok() +} + // extract doc line from attribute fn extract_doc_line(existing_docs: Option, attr: &syn::Attribute) -> Option { if !attr.path().is_ident("doc") { @@ -192,12 +259,22 @@ pub fn tool(attr: TokenStream, input: TokenStream) -> syn::Result { } else { none_expr() }; + // Handle output_schema - either explicit or generated from return type + let output_schema_expr = attribute.output_schema.or_else(|| { + // Try to generate schema from return type + match &fn_item.sig.output { + syn::ReturnType::Type(_, ret_type) => extract_schema_from_return_type(ret_type), + _ => None, + } + }); + let resolved_tool_attr = ResolvedToolAttribute { name: attribute.name.unwrap_or_else(|| fn_ident.to_string()), description: attribute .description .or_else(|| fn_item.attrs.iter().fold(None, extract_doc_line)), input_schema: input_schema_expr, + output_schema: output_schema_expr, annotations: annotations_expr, }; let tool_attr_fn = resolved_tool_attr.into_fn(tool_attr_fn_ident)?; diff --git a/crates/rmcp/src/handler/server/router/tool.rs b/crates/rmcp/src/handler/server/router/tool.rs index 9735d28f..2a100de9 100644 --- a/crates/rmcp/src/handler/server/router/tool.rs +++ b/crates/rmcp/src/handler/server/router/tool.rs @@ -6,6 +6,7 @@ use schemars::JsonSchema; use crate::{ handler::server::tool::{ CallToolHandler, DynCallToolHandler, ToolCallContext, schema_for_type, + validate_against_schema, }, model::{CallToolResult, Tool, ToolAnnotations}, }; @@ -242,7 +243,23 @@ where .map .get(context.name()) .ok_or_else(|| crate::ErrorData::invalid_params("tool not found", None))?; - (item.call)(context).await + + let result = (item.call)(context).await?; + + // Validate structured content against output schema if present + if let Some(ref output_schema) = item.attr.output_schema { + // When output_schema is defined, structured_content is required + if result.structured_content.is_none() { + return Err(crate::ErrorData::invalid_params( + "Tool with output_schema must return structured_content", + None, + )); + } + // Validate the structured content against the schema + validate_against_schema(result.structured_content.as_ref().unwrap(), output_schema)?; + } + + Ok(result) } pub fn list_all(&self) -> Vec { diff --git a/crates/rmcp/src/handler/server/tool.rs b/crates/rmcp/src/handler/server/tool.rs index cea0e9cc..8d5c8213 100644 --- a/crates/rmcp/src/handler/server/tool.rs +++ b/crates/rmcp/src/handler/server/tool.rs @@ -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`, 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, 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() -> 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( + 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() -> Arc { thread_local! { @@ -97,8 +171,26 @@ pub trait FromToolCallContextPart: Sized { ) -> Result; } +/// Trait for converting tool return values into [`CallToolResult`]. +/// +/// This trait is automatically implemented for: +/// - Types implementing [`IntoContents`] (returns unstructured content) +/// - `Result` where both `T` and `E` implement [`IntoContents`] +/// - [`Json`](crate::handler::server::wrapper::Json) where `T` implements [`Serialize`] (returns structured content) +/// - `Result, 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; + + /// 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> { + None + } } impl IntoCallToolResult for T { @@ -125,6 +217,40 @@ impl IntoCallToolResult for Result { } } +// Implementation for Json to create structured content +impl IntoCallToolResult for Json { + fn into_call_tool_result(self) -> Result { + 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> { + Some(cached_schema_for_type::()) + } +} + +// Implementation for Result, E> +impl IntoCallToolResult + for Result, E> +{ + fn into_call_tool_result(self) -> Result { + match self { + Ok(value) => value.into_call_tool_result(), + Err(error) => Ok(CallToolResult::error(error.into_contents())), + } + } + + fn output_schema() -> Option> { + Json::::output_schema() + } +} + pin_project_lite::pin_project! { #[project = IntoCallToolResultFutProj] pub enum IntoCallToolResultFut { diff --git a/crates/rmcp/src/handler/server/wrapper/json.rs b/crates/rmcp/src/handler/server/wrapper/json.rs index c1c85740..25f1d3f5 100644 --- a/crates/rmcp/src/handler/server/wrapper/json.rs +++ b/crates/rmcp/src/handler/server/wrapper/json.rs @@ -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(pub T); -impl IntoContents for Json -where - T: Serialize, -{ - fn into_contents(self) -> Vec { - 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 to delegate to T's schema +impl JsonSchema for Json { + fn schema_name() -> Cow<'static, str> { + T::schema_name() + } + + fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + T::json_schema(generator) } } diff --git a/crates/rmcp/src/lib.rs b/crates/rmcp/src/lib.rs index d5dbaad1..054008df 100644 --- a/crates/rmcp/src/lib.rs +++ b/crates/rmcp/src/lib.rs @@ -48,8 +48,52 @@ //! } //! ``` //! -//! Next also implement [ServerHandler] for `Counter` and start the server inside -//! `main` by calling `Counter::new().serve(...)`. See the examples directory in the repository for more information. +//! ### Structured Output +//! +//! Tools can also return structured JSON data with schemas. Use the [`Json`] wrapper: +//! +//! ```rust +//! # use rmcp::{tool, tool_router, handler::server::tool::{ToolRouter, Parameters}, Json}; +//! # use schemars::JsonSchema; +//! # use serde::{Serialize, Deserialize}; +//! # +//! #[derive(Serialize, Deserialize, JsonSchema)] +//! struct CalculationRequest { +//! a: i32, +//! b: i32, +//! operation: String, +//! } +//! +//! #[derive(Serialize, Deserialize, JsonSchema)] +//! struct CalculationResult { +//! result: i32, +//! operation: String, +//! } +//! +//! # #[derive(Clone)] +//! # struct Calculator { +//! # tool_router: ToolRouter, +//! # } +//! # +//! # #[tool_router] +//! # impl Calculator { +//! #[tool(name = "calculate", description = "Perform a calculation")] +//! async fn calculate(&self, params: Parameters) -> Result, String> { +//! let result = match params.0.operation.as_str() { +//! "add" => params.0.a + params.0.b, +//! "multiply" => params.0.a * params.0.b, +//! _ => return Err("Unknown operation".to_string()), +//! }; +//! +//! Ok(Json(CalculationResult { result, operation: params.0.operation })) +//! } +//! # } +//! ``` +//! +//! The `#[tool]` macro automatically generates an output schema from the `CalculationResult` type. +//! +//! Next also implement [ServerHandler] for your server type and start the server inside +//! `main` by calling `.serve(...)`. See the examples directory in the repository for more information. //! //! ## Client //! @@ -104,6 +148,9 @@ pub use handler::client::ClientHandler; #[cfg(feature = "server")] #[cfg_attr(docsrs, doc(cfg(feature = "server")))] pub use handler::server::ServerHandler; +#[cfg(feature = "server")] +#[cfg_attr(docsrs, doc(cfg(feature = "server")))] +pub use handler::server::wrapper::Json; #[cfg(any(feature = "client", feature = "server"))] #[cfg_attr(docsrs, doc(cfg(any(feature = "client", feature = "server"))))] pub use service::{Peer, Service, ServiceError, ServiceExt}; diff --git a/crates/rmcp/src/model.rs b/crates/rmcp/src/model.rs index a28a1fba..f0c92c0a 100644 --- a/crates/rmcp/src/model.rs +++ b/crates/rmcp/src/model.rs @@ -1181,32 +1181,126 @@ pub type RootsListChangedNotification = NotificationNoParam, + #[serde(skip_serializing_if = "Option::is_none")] + pub content: Option>, + /// An optional JSON object that represents the structured result of the tool call + #[serde(skip_serializing_if = "Option::is_none")] + pub structured_content: Option, /// Whether this result represents an error condition #[serde(skip_serializing_if = "Option::is_none")] pub is_error: Option, } impl CallToolResult { - /// Create a successful tool result + /// Create a successful tool result with unstructured content pub fn success(content: Vec) -> Self { CallToolResult { - content, + content: Some(content), + structured_content: None, is_error: Some(false), } } - /// Create an error tool result + /// Create an error tool result with unstructured content pub fn error(content: Vec) -> Self { CallToolResult { - content, + content: Some(content), + structured_content: None, + is_error: Some(true), + } + } + /// Create a successful tool result with structured content + /// + /// # Example + /// + /// ```rust,ignore + /// use rmcp::model::CallToolResult; + /// use serde_json::json; + /// + /// let result = CallToolResult::structured(json!({ + /// "temperature": 22.5, + /// "humidity": 65, + /// "description": "Partly cloudy" + /// })); + /// ``` + pub fn structured(value: Value) -> Self { + CallToolResult { + content: None, + structured_content: Some(value), + is_error: Some(false), + } + } + /// Create an error tool result with structured content + /// + /// # Example + /// + /// ```rust,ignore + /// use rmcp::model::CallToolResult; + /// use serde_json::json; + /// + /// let result = CallToolResult::structured_error(json!({ + /// "error_code": "INVALID_INPUT", + /// "message": "Temperature value out of range", + /// "details": { + /// "min": -50, + /// "max": 50, + /// "provided": 100 + /// } + /// })); + /// ``` + pub fn structured_error(value: Value) -> Self { + CallToolResult { + content: None, + structured_content: Some(value), is_error: Some(true), } } + + /// Validate that content and structured_content are mutually exclusive + pub fn validate(&self) -> Result<(), &'static str> { + match (&self.content, &self.structured_content) { + (Some(_), Some(_)) => Err("content and structured_content are mutually exclusive"), + (None, None) => Err("either content or structured_content must be provided"), + _ => Ok(()), + } + } +} + +// Custom deserialize implementation to validate mutual exclusivity +impl<'de> Deserialize<'de> for CallToolResult { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct CallToolResultHelper { + #[serde(skip_serializing_if = "Option::is_none")] + content: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + structured_content: Option, + #[serde(skip_serializing_if = "Option::is_none")] + is_error: Option, + } + + let helper = CallToolResultHelper::deserialize(deserializer)?; + let result = CallToolResult { + content: helper.content, + structured_content: helper.structured_content, + is_error: helper.is_error, + }; + + // Validate mutual exclusivity + result.validate().map_err(serde::de::Error::custom)?; + + Ok(result) + } } const_string!(ListToolsRequestMethod = "tools/list"); diff --git a/crates/rmcp/src/model/tool.rs b/crates/rmcp/src/model/tool.rs index e24cf44c..b2ea4bf8 100644 --- a/crates/rmcp/src/model/tool.rs +++ b/crates/rmcp/src/model/tool.rs @@ -1,5 +1,6 @@ use std::{borrow::Cow, sync::Arc}; +use schemars::JsonSchema; /// Tools represent a routine that a server can execute /// Tool calls represent requests from the client to execute one use serde::{Deserialize, Serialize}; @@ -19,6 +20,9 @@ pub struct Tool { pub description: Option>, /// A JSON Schema object defining the expected parameters for the tool pub input_schema: Arc, + /// An optional JSON Schema object defining the structure of the tool's output + #[serde(skip_serializing_if = "Option::is_none")] + pub output_schema: Option>, #[serde(skip_serializing_if = "Option::is_none")] /// Optional additional tool information. pub annotations: Option, @@ -136,6 +140,7 @@ impl Tool { name: name.into(), description: Some(description.into()), input_schema: input_schema.into(), + output_schema: None, annotations: None, } } @@ -147,6 +152,18 @@ impl Tool { } } + /// Set the output schema using a type that implements JsonSchema + pub fn with_output_schema(mut self) -> Self { + self.output_schema = Some(crate::handler::server::tool::cached_schema_for_type::()); + self + } + + /// Set the input schema using a type that implements JsonSchema + pub fn with_input_schema(mut self) -> Self { + self.input_schema = crate::handler::server::tool::cached_schema_for_type::(); + self + } + /// Get the schema as json value pub fn schema_as_json_value(&self) -> Value { Value::Object(self.input_schema.as_ref().clone()) diff --git a/crates/rmcp/tests/test_json_schema_detection.rs b/crates/rmcp/tests/test_json_schema_detection.rs new file mode 100644 index 00000000..89dd8586 --- /dev/null +++ b/crates/rmcp/tests/test_json_schema_detection.rs @@ -0,0 +1,114 @@ +//cargo test --test test_json_schema_detection --features "client server macros" +use rmcp::{ + Json, ServerHandler, handler::server::router::tool::ToolRouter, tool, tool_handler, tool_router, +}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct TestData { + pub value: String, +} + +#[tool_handler(router = self.tool_router)] +impl ServerHandler for TestServer {} + +#[derive(Debug, Clone)] +pub struct TestServer { + tool_router: ToolRouter, +} + +impl Default for TestServer { + fn default() -> Self { + Self::new() + } +} + +#[tool_router(router = tool_router)] +impl TestServer { + pub fn new() -> Self { + Self { + tool_router: Self::tool_router(), + } + } + + /// Tool that returns Json - should have output schema + #[tool(name = "with-json")] + pub async fn with_json(&self) -> Result, String> { + Ok(Json(TestData { + value: "test".to_string(), + })) + } + + /// Tool that returns regular type - should NOT have output schema + #[tool(name = "without-json")] + pub async fn without_json(&self) -> Result { + Ok("test".to_string()) + } + + /// Tool that returns Result with inner Json - should have output schema + #[tool(name = "result-with-json")] + pub async fn result_with_json(&self) -> Result, rmcp::ErrorData> { + Ok(Json(TestData { + value: "test".to_string(), + })) + } + + /// Tool with explicit output_schema attribute - should have output schema + #[tool(name = "explicit-schema", output_schema = rmcp::handler::server::tool::cached_schema_for_type::())] + pub async fn explicit_schema(&self) -> Result { + Ok("test".to_string()) + } +} + +#[tokio::test] +async fn test_json_type_generates_schema() { + let server = TestServer::new(); + let tools = server.tool_router.list_all(); + + // Find the with-json tool + let json_tool = tools.iter().find(|t| t.name == "with-json").unwrap(); + assert!( + json_tool.output_schema.is_some(), + "Json return type should generate output schema" + ); +} + +#[tokio::test] +async fn test_non_json_type_no_schema() { + let server = TestServer::new(); + let tools = server.tool_router.list_all(); + + // Find the without-json tool + let non_json_tool = tools.iter().find(|t| t.name == "without-json").unwrap(); + assert!( + non_json_tool.output_schema.is_none(), + "Regular return type should NOT generate output schema" + ); +} + +#[tokio::test] +async fn test_result_with_json_generates_schema() { + let server = TestServer::new(); + let tools = server.tool_router.list_all(); + + // Find the result-with-json tool + let result_json_tool = tools.iter().find(|t| t.name == "result-with-json").unwrap(); + assert!( + result_json_tool.output_schema.is_some(), + "Result, E> return type should generate output schema" + ); +} + +#[tokio::test] +async fn test_explicit_schema_override() { + let server = TestServer::new(); + let tools = server.tool_router.list_all(); + + // Find the explicit-schema tool + let explicit_tool = tools.iter().find(|t| t.name == "explicit-schema").unwrap(); + assert!( + explicit_tool.output_schema.is_some(), + "Explicit output_schema attribute should work" + ); +} diff --git a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json index 5cac39cb..aaa5562e 100644 --- a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json +++ b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json @@ -299,12 +299,15 @@ } }, "CallToolResult": { - "description": "The result of a tool call operation.\n\nContains the content returned by the tool execution and an optional\nflag indicating whether the operation resulted in an error.", + "description": "The result of a tool call operation.\n\nContains the content returned by the tool execution and an optional\nflag indicating whether the operation resulted in an error.\n\nNote: `content` and `structured_content` are mutually exclusive - exactly one must be provided.", "type": "object", "properties": { "content": { "description": "The content returned by the tool (text, images, etc.)", - "type": "array", + "type": [ + "array", + "null" + ], "items": { "$ref": "#/definitions/Annotated" } @@ -315,11 +318,11 @@ "boolean", "null" ] + }, + "structuredContent": { + "description": "An optional JSON object that represents the structured result of the tool call" } - }, - "required": [ - "content" - ] + } }, "CancelledNotificationMethod": { "type": "string", @@ -1580,6 +1583,14 @@ "name": { "description": "The name of the tool", "type": "string" + }, + "outputSchema": { + "description": "An optional JSON Schema object defining the structure of the tool's output", + "type": [ + "object", + "null" + ], + "additionalProperties": true } }, "required": [ diff --git a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json new file mode 100644 index 00000000..aaa5562e --- /dev/null +++ b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json @@ -0,0 +1,1659 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "JsonRpcMessage", + "description": "Represents any JSON-RPC message that can be sent or received.\n\nThis enum covers all possible message types in the JSON-RPC protocol:\nindividual requests/responses, notifications, batch operations, and errors.\nIt serves as the top-level message container for MCP communication.", + "anyOf": [ + { + "description": "A single request expecting a response", + "allOf": [ + { + "$ref": "#/definitions/JsonRpcRequest" + } + ] + }, + { + "description": "A response to a previous request", + "allOf": [ + { + "$ref": "#/definitions/JsonRpcResponse" + } + ] + }, + { + "description": "A one-way notification (no response expected)", + "allOf": [ + { + "$ref": "#/definitions/JsonRpcNotification" + } + ] + }, + { + "description": "Multiple requests sent together", + "type": "array", + "items": { + "$ref": "#/definitions/JsonRpcBatchRequestItem" + } + }, + { + "description": "Multiple responses sent together", + "type": "array", + "items": { + "$ref": "#/definitions/JsonRpcBatchResponseItem" + } + }, + { + "description": "An error response", + "allOf": [ + { + "$ref": "#/definitions/JsonRpcError" + } + ] + } + ], + "definitions": { + "Annotated": { + "type": "object", + "properties": { + "annotations": { + "anyOf": [ + { + "$ref": "#/definitions/Annotations" + }, + { + "type": "null" + } + ] + } + }, + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "text" + } + }, + "allOf": [ + { + "$ref": "#/definitions/RawTextContent" + } + ], + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "image" + } + }, + "allOf": [ + { + "$ref": "#/definitions/RawImageContent" + } + ], + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "resource" + } + }, + "allOf": [ + { + "$ref": "#/definitions/RawEmbeddedResource" + } + ], + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "audio" + } + }, + "allOf": [ + { + "$ref": "#/definitions/Annotated2" + } + ], + "required": [ + "type" + ] + } + ] + }, + "Annotated2": { + "type": "object", + "properties": { + "annotations": { + "anyOf": [ + { + "$ref": "#/definitions/Annotations" + }, + { + "type": "null" + } + ] + }, + "data": { + "type": "string" + }, + "mimeType": { + "type": "string" + } + }, + "required": [ + "data", + "mimeType" + ] + }, + "Annotated3": { + "type": "object", + "properties": { + "annotations": { + "anyOf": [ + { + "$ref": "#/definitions/Annotations" + }, + { + "type": "null" + } + ] + }, + "resource": { + "$ref": "#/definitions/ResourceContents" + } + }, + "required": [ + "resource" + ] + }, + "Annotated4": { + "description": "Represents a resource in the extension with metadata", + "type": "object", + "properties": { + "annotations": { + "anyOf": [ + { + "$ref": "#/definitions/Annotations" + }, + { + "type": "null" + } + ] + }, + "description": { + "description": "Optional description of the resource", + "type": [ + "string", + "null" + ] + }, + "mimeType": { + "description": "MIME type of the resource content (\"text\" or \"blob\")", + "type": [ + "string", + "null" + ] + }, + "name": { + "description": "Name of the resource", + "type": "string" + }, + "size": { + "description": "The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known.\n\nThis can be used by Hosts to display file sizes and estimate context window us", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0 + }, + "uri": { + "description": "URI representing the resource location (e.g., \"file:///path/to/file\" or \"str:///content\")", + "type": "string" + } + }, + "required": [ + "uri", + "name" + ] + }, + "Annotated5": { + "type": "object", + "properties": { + "annotations": { + "anyOf": [ + { + "$ref": "#/definitions/Annotations" + }, + { + "type": "null" + } + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "mimeType": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": "string" + }, + "uriTemplate": { + "type": "string" + } + }, + "required": [ + "uriTemplate", + "name" + ] + }, + "Annotations": { + "type": "object", + "properties": { + "audience": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Role" + } + }, + "priority": { + "type": [ + "number", + "null" + ], + "format": "float" + }, + "timestamp": { + "type": [ + "string", + "null" + ], + "format": "date-time" + } + } + }, + "CallToolResult": { + "description": "The result of a tool call operation.\n\nContains the content returned by the tool execution and an optional\nflag indicating whether the operation resulted in an error.\n\nNote: `content` and `structured_content` are mutually exclusive - exactly one must be provided.", + "type": "object", + "properties": { + "content": { + "description": "The content returned by the tool (text, images, etc.)", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Annotated" + } + }, + "isError": { + "description": "Whether this result represents an error condition", + "type": [ + "boolean", + "null" + ] + }, + "structuredContent": { + "description": "An optional JSON object that represents the structured result of the tool call" + } + } + }, + "CancelledNotificationMethod": { + "type": "string", + "format": "const", + "const": "notifications/cancelled" + }, + "CancelledNotificationParam": { + "type": "object", + "properties": { + "reason": { + "type": [ + "string", + "null" + ] + }, + "requestId": { + "$ref": "#/definitions/NumberOrString" + } + }, + "required": [ + "requestId" + ] + }, + "CompleteResult": { + "type": "object", + "properties": { + "completion": { + "$ref": "#/definitions/CompletionInfo" + } + }, + "required": [ + "completion" + ] + }, + "CompletionInfo": { + "type": "object", + "properties": { + "hasMore": { + "type": [ + "boolean", + "null" + ] + }, + "total": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0 + }, + "values": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "values" + ] + }, + "ContextInclusion": { + "description": "Specifies how much context should be included in sampling requests.\n\nThis allows clients to control what additional context information\nshould be provided to the LLM when processing sampling requests.", + "oneOf": [ + { + "description": "Include context from all connected MCP servers", + "type": "string", + "const": "allServers" + }, + { + "description": "Include no additional context", + "type": "string", + "const": "none" + }, + { + "description": "Include context only from the requesting server", + "type": "string", + "const": "thisServer" + } + ] + }, + "CreateMessageRequestMethod": { + "type": "string", + "format": "const", + "const": "sampling/createMessage" + }, + "CreateMessageRequestParam": { + "description": "Parameters for creating a message through LLM sampling.\n\nThis structure contains all the necessary information for a client to\ngenerate an LLM response, including conversation history, model preferences,\nand generation parameters.", + "type": "object", + "properties": { + "includeContext": { + "description": "How much context to include from MCP servers", + "anyOf": [ + { + "$ref": "#/definitions/ContextInclusion" + }, + { + "type": "null" + } + ] + }, + "maxTokens": { + "description": "Maximum number of tokens to generate", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "messages": { + "description": "The conversation history and current messages", + "type": "array", + "items": { + "$ref": "#/definitions/SamplingMessage" + } + }, + "metadata": { + "description": "Additional metadata for the request" + }, + "modelPreferences": { + "description": "Preferences for model selection and behavior", + "anyOf": [ + { + "$ref": "#/definitions/ModelPreferences" + }, + { + "type": "null" + } + ] + }, + "stopSequences": { + "description": "Sequences that should stop generation", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "systemPrompt": { + "description": "System prompt to guide the model's behavior", + "type": [ + "string", + "null" + ] + }, + "temperature": { + "description": "Temperature for controlling randomness (0.0 to 1.0)", + "type": [ + "number", + "null" + ], + "format": "float" + } + }, + "required": [ + "messages", + "maxTokens" + ] + }, + "EmptyObject": { + "description": "This is commonly used for representing empty objects in MCP messages.\n\nwithout returning any specific data.", + "type": "object" + }, + "ErrorCode": { + "description": "Standard JSON-RPC error codes used throughout the MCP protocol.\n\nThese codes follow the JSON-RPC 2.0 specification and provide\nstandardized error reporting across all MCP implementations.", + "type": "integer", + "format": "int32" + }, + "ErrorData": { + "description": "Error information for JSON-RPC error responses.\n\nThis structure follows the JSON-RPC 2.0 specification for error reporting,\nproviding a standardized way to communicate errors between clients and servers.", + "type": "object", + "properties": { + "code": { + "description": "The error type that occurred (using standard JSON-RPC error codes)", + "allOf": [ + { + "$ref": "#/definitions/ErrorCode" + } + ] + }, + "data": { + "description": "Additional information about the error. The value of this member is defined by the\nsender (e.g. detailed error information, nested errors etc.)." + }, + "message": { + "description": "A short description of the error. The message SHOULD be limited to a concise single sentence.", + "type": "string" + } + }, + "required": [ + "code", + "message" + ] + }, + "GetPromptResult": { + "type": "object", + "properties": { + "description": { + "type": [ + "string", + "null" + ] + }, + "messages": { + "type": "array", + "items": { + "$ref": "#/definitions/PromptMessage" + } + } + }, + "required": [ + "messages" + ] + }, + "Implementation": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + } + }, + "required": [ + "name", + "version" + ] + }, + "InitializeResult": { + "description": "The server's response to an initialization request.\n\nContains the server's protocol version, capabilities, and implementation\ninformation, along with optional instructions for the client.", + "type": "object", + "properties": { + "capabilities": { + "description": "The capabilities this server provides (tools, resources, prompts, etc.)", + "allOf": [ + { + "$ref": "#/definitions/ServerCapabilities" + } + ] + }, + "instructions": { + "description": "Optional human-readable instructions about using this server", + "type": [ + "string", + "null" + ] + }, + "protocolVersion": { + "description": "The MCP protocol version this server supports", + "allOf": [ + { + "$ref": "#/definitions/ProtocolVersion" + } + ] + }, + "serverInfo": { + "description": "Information about the server implementation", + "allOf": [ + { + "$ref": "#/definitions/Implementation" + } + ] + } + }, + "required": [ + "protocolVersion", + "capabilities", + "serverInfo" + ] + }, + "JsonRpcBatchRequestItem": { + "anyOf": [ + { + "$ref": "#/definitions/JsonRpcRequest" + }, + { + "$ref": "#/definitions/JsonRpcNotification" + } + ] + }, + "JsonRpcBatchResponseItem": { + "anyOf": [ + { + "$ref": "#/definitions/JsonRpcResponse" + }, + { + "$ref": "#/definitions/JsonRpcError" + } + ] + }, + "JsonRpcError": { + "type": "object", + "properties": { + "error": { + "$ref": "#/definitions/ErrorData" + }, + "id": { + "$ref": "#/definitions/NumberOrString" + }, + "jsonrpc": { + "$ref": "#/definitions/JsonRpcVersion2_0" + } + }, + "required": [ + "jsonrpc", + "id", + "error" + ] + }, + "JsonRpcNotification": { + "type": "object", + "properties": { + "jsonrpc": { + "$ref": "#/definitions/JsonRpcVersion2_0" + } + }, + "anyOf": [ + { + "$ref": "#/definitions/Notification" + }, + { + "$ref": "#/definitions/Notification2" + }, + { + "$ref": "#/definitions/Notification3" + }, + { + "$ref": "#/definitions/Notification4" + }, + { + "$ref": "#/definitions/NotificationNoParam" + }, + { + "$ref": "#/definitions/NotificationNoParam2" + }, + { + "$ref": "#/definitions/NotificationNoParam3" + } + ], + "required": [ + "jsonrpc" + ] + }, + "JsonRpcRequest": { + "type": "object", + "properties": { + "id": { + "$ref": "#/definitions/NumberOrString" + }, + "jsonrpc": { + "$ref": "#/definitions/JsonRpcVersion2_0" + } + }, + "anyOf": [ + { + "$ref": "#/definitions/RequestNoParam" + }, + { + "$ref": "#/definitions/Request" + }, + { + "$ref": "#/definitions/RequestNoParam2" + } + ], + "required": [ + "jsonrpc", + "id" + ] + }, + "JsonRpcResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/definitions/NumberOrString" + }, + "jsonrpc": { + "$ref": "#/definitions/JsonRpcVersion2_0" + }, + "result": { + "$ref": "#/definitions/ServerResult" + } + }, + "required": [ + "jsonrpc", + "id", + "result" + ] + }, + "JsonRpcVersion2_0": { + "type": "string", + "format": "const", + "const": "2.0" + }, + "ListPromptsResult": { + "type": "object", + "properties": { + "nextCursor": { + "type": [ + "string", + "null" + ] + }, + "prompts": { + "type": "array", + "items": { + "$ref": "#/definitions/Prompt" + } + } + }, + "required": [ + "prompts" + ] + }, + "ListResourceTemplatesResult": { + "type": "object", + "properties": { + "nextCursor": { + "type": [ + "string", + "null" + ] + }, + "resourceTemplates": { + "type": "array", + "items": { + "$ref": "#/definitions/Annotated5" + } + } + }, + "required": [ + "resourceTemplates" + ] + }, + "ListResourcesResult": { + "type": "object", + "properties": { + "nextCursor": { + "type": [ + "string", + "null" + ] + }, + "resources": { + "type": "array", + "items": { + "$ref": "#/definitions/Annotated4" + } + } + }, + "required": [ + "resources" + ] + }, + "ListRootsRequestMethod": { + "type": "string", + "format": "const", + "const": "roots/list" + }, + "ListToolsResult": { + "type": "object", + "properties": { + "nextCursor": { + "type": [ + "string", + "null" + ] + }, + "tools": { + "type": "array", + "items": { + "$ref": "#/definitions/Tool" + } + } + }, + "required": [ + "tools" + ] + }, + "LoggingLevel": { + "description": "Logging levels supported by the MCP protocol", + "type": "string", + "enum": [ + "debug", + "info", + "notice", + "warning", + "error", + "critical", + "alert", + "emergency" + ] + }, + "LoggingMessageNotificationMethod": { + "type": "string", + "format": "const", + "const": "notifications/message" + }, + "LoggingMessageNotificationParam": { + "description": "Parameters for a logging message notification", + "type": "object", + "properties": { + "data": { + "description": "The actual log data" + }, + "level": { + "description": "The severity level of this log message", + "allOf": [ + { + "$ref": "#/definitions/LoggingLevel" + } + ] + }, + "logger": { + "description": "Optional logger name that generated this message", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "level", + "data" + ] + }, + "ModelHint": { + "description": "A hint suggesting a preferred model name or family.\n\nModel hints are advisory suggestions that help clients choose appropriate\nmodels. They can be specific model names or general families like \"claude\" or \"gpt\".", + "type": "object", + "properties": { + "name": { + "description": "The suggested model name or family identifier", + "type": [ + "string", + "null" + ] + } + } + }, + "ModelPreferences": { + "description": "Preferences for model selection and behavior in sampling requests.\n\nThis allows servers to express their preferences for which model to use\nand how to balance different priorities when the client has multiple\nmodel options available.", + "type": "object", + "properties": { + "costPriority": { + "description": "Priority for cost optimization (0.0 to 1.0, higher = prefer cheaper models)", + "type": [ + "number", + "null" + ], + "format": "float" + }, + "hints": { + "description": "Specific model names or families to prefer (e.g., \"claude\", \"gpt\")", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/ModelHint" + } + }, + "intelligencePriority": { + "description": "Priority for intelligence/capability (0.0 to 1.0, higher = prefer more capable models)", + "type": [ + "number", + "null" + ], + "format": "float" + }, + "speedPriority": { + "description": "Priority for speed/latency (0.0 to 1.0, higher = prefer faster models)", + "type": [ + "number", + "null" + ], + "format": "float" + } + } + }, + "Notification": { + "type": "object", + "properties": { + "method": { + "$ref": "#/definitions/CancelledNotificationMethod" + }, + "params": { + "$ref": "#/definitions/CancelledNotificationParam" + } + }, + "required": [ + "method", + "params" + ] + }, + "Notification2": { + "type": "object", + "properties": { + "method": { + "$ref": "#/definitions/ProgressNotificationMethod" + }, + "params": { + "$ref": "#/definitions/ProgressNotificationParam" + } + }, + "required": [ + "method", + "params" + ] + }, + "Notification3": { + "type": "object", + "properties": { + "method": { + "$ref": "#/definitions/LoggingMessageNotificationMethod" + }, + "params": { + "$ref": "#/definitions/LoggingMessageNotificationParam" + } + }, + "required": [ + "method", + "params" + ] + }, + "Notification4": { + "type": "object", + "properties": { + "method": { + "$ref": "#/definitions/ResourceUpdatedNotificationMethod" + }, + "params": { + "$ref": "#/definitions/ResourceUpdatedNotificationParam" + } + }, + "required": [ + "method", + "params" + ] + }, + "NotificationNoParam": { + "type": "object", + "properties": { + "method": { + "$ref": "#/definitions/ResourceListChangedNotificationMethod" + } + }, + "required": [ + "method" + ] + }, + "NotificationNoParam2": { + "type": "object", + "properties": { + "method": { + "$ref": "#/definitions/ToolListChangedNotificationMethod" + } + }, + "required": [ + "method" + ] + }, + "NotificationNoParam3": { + "type": "object", + "properties": { + "method": { + "$ref": "#/definitions/PromptListChangedNotificationMethod" + } + }, + "required": [ + "method" + ] + }, + "NumberOrString": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "string" + } + ] + }, + "PingRequestMethod": { + "type": "string", + "format": "const", + "const": "ping" + }, + "ProgressNotificationMethod": { + "type": "string", + "format": "const", + "const": "notifications/progress" + }, + "ProgressNotificationParam": { + "type": "object", + "properties": { + "message": { + "description": "An optional message describing the current progress.", + "type": [ + "string", + "null" + ] + }, + "progress": { + "description": "The progress thus far. This should increase every time progress is made, even if the total is unknown.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "progressToken": { + "$ref": "#/definitions/ProgressToken" + }, + "total": { + "description": "Total number of items to process (or total progress required), if known", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "progressToken", + "progress" + ] + }, + "ProgressToken": { + "description": "A token used to track the progress of long-running operations.\n\nProgress tokens allow clients and servers to associate progress notifications\nwith specific requests, enabling real-time updates on operation status.", + "allOf": [ + { + "$ref": "#/definitions/NumberOrString" + } + ] + }, + "Prompt": { + "description": "A prompt that can be used to generate text from a model", + "type": "object", + "properties": { + "arguments": { + "description": "Optional arguments that can be passed to customize the prompt", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/PromptArgument" + } + }, + "description": { + "description": "Optional description of what the prompt does", + "type": [ + "string", + "null" + ] + }, + "name": { + "description": "The name of the prompt", + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "PromptArgument": { + "description": "Represents a prompt argument that can be passed to customize the prompt", + "type": "object", + "properties": { + "description": { + "description": "A description of what the argument is used for", + "type": [ + "string", + "null" + ] + }, + "name": { + "description": "The name of the argument", + "type": "string" + }, + "required": { + "description": "Whether this argument is required", + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "name" + ] + }, + "PromptListChangedNotificationMethod": { + "type": "string", + "format": "const", + "const": "notifications/prompts/list_changed" + }, + "PromptMessage": { + "description": "A message in a prompt conversation", + "type": "object", + "properties": { + "content": { + "description": "The content of the message", + "allOf": [ + { + "$ref": "#/definitions/PromptMessageContent" + } + ] + }, + "role": { + "description": "The role of the message sender", + "allOf": [ + { + "$ref": "#/definitions/PromptMessageRole" + } + ] + } + }, + "required": [ + "role", + "content" + ] + }, + "PromptMessageContent": { + "description": "Content types that can be included in prompt messages", + "oneOf": [ + { + "description": "Plain text content", + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "type": { + "type": "string", + "const": "text" + } + }, + "required": [ + "type", + "text" + ] + }, + { + "description": "Image content with base64-encoded data", + "type": "object", + "properties": { + "annotations": { + "anyOf": [ + { + "$ref": "#/definitions/Annotations" + }, + { + "type": "null" + } + ] + }, + "data": { + "description": "The base64-encoded image", + "type": "string" + }, + "mimeType": { + "type": "string" + }, + "type": { + "type": "string", + "const": "image" + } + }, + "required": [ + "type", + "data", + "mimeType" + ] + }, + { + "description": "Embedded server-side resource", + "type": "object", + "properties": { + "resource": { + "$ref": "#/definitions/Annotated3" + }, + "type": { + "type": "string", + "const": "resource" + } + }, + "required": [ + "type", + "resource" + ] + } + ] + }, + "PromptMessageRole": { + "description": "Represents the role of a message sender in a prompt conversation", + "type": "string", + "enum": [ + "user", + "assistant" + ] + }, + "PromptsCapability": { + "type": "object", + "properties": { + "listChanged": { + "type": [ + "boolean", + "null" + ] + } + } + }, + "ProtocolVersion": { + "description": "Represents the MCP protocol version used for communication.\n\nThis ensures compatibility between clients and servers by specifying\nwhich version of the Model Context Protocol is being used.", + "type": "string" + }, + "RawEmbeddedResource": { + "type": "object", + "properties": { + "resource": { + "$ref": "#/definitions/ResourceContents" + } + }, + "required": [ + "resource" + ] + }, + "RawImageContent": { + "type": "object", + "properties": { + "data": { + "description": "The base64-encoded image", + "type": "string" + }, + "mimeType": { + "type": "string" + } + }, + "required": [ + "data", + "mimeType" + ] + }, + "RawTextContent": { + "type": "object", + "properties": { + "text": { + "type": "string" + } + }, + "required": [ + "text" + ] + }, + "ReadResourceResult": { + "description": "Result containing the contents of a read resource", + "type": "object", + "properties": { + "contents": { + "description": "The actual content of the resource", + "type": "array", + "items": { + "$ref": "#/definitions/ResourceContents" + } + } + }, + "required": [ + "contents" + ] + }, + "Request": { + "description": "Represents a JSON-RPC request with method, parameters, and extensions.\n\nThis is the core structure for all MCP requests, containing:\n- `method`: The name of the method being called\n- `params`: The parameters for the method\n- `extensions`: Additional context data (similar to HTTP headers)", + "type": "object", + "properties": { + "method": { + "$ref": "#/definitions/CreateMessageRequestMethod" + }, + "params": { + "$ref": "#/definitions/CreateMessageRequestParam" + } + }, + "required": [ + "method", + "params" + ] + }, + "RequestNoParam": { + "type": "object", + "properties": { + "method": { + "$ref": "#/definitions/PingRequestMethod" + } + }, + "required": [ + "method" + ] + }, + "RequestNoParam2": { + "type": "object", + "properties": { + "method": { + "$ref": "#/definitions/ListRootsRequestMethod" + } + }, + "required": [ + "method" + ] + }, + "ResourceContents": { + "anyOf": [ + { + "type": "object", + "properties": { + "mime_type": { + "type": [ + "string", + "null" + ] + }, + "text": { + "type": "string" + }, + "uri": { + "type": "string" + } + }, + "required": [ + "uri", + "text" + ] + }, + { + "type": "object", + "properties": { + "blob": { + "type": "string" + }, + "mime_type": { + "type": [ + "string", + "null" + ] + }, + "uri": { + "type": "string" + } + }, + "required": [ + "uri", + "blob" + ] + } + ] + }, + "ResourceListChangedNotificationMethod": { + "type": "string", + "format": "const", + "const": "notifications/resources/list_changed" + }, + "ResourceUpdatedNotificationMethod": { + "type": "string", + "format": "const", + "const": "notifications/resources/updated" + }, + "ResourceUpdatedNotificationParam": { + "description": "Parameters for a resource update notification", + "type": "object", + "properties": { + "uri": { + "description": "The URI of the resource that was updated", + "type": "string" + } + }, + "required": [ + "uri" + ] + }, + "ResourcesCapability": { + "type": "object", + "properties": { + "listChanged": { + "type": [ + "boolean", + "null" + ] + }, + "subscribe": { + "type": [ + "boolean", + "null" + ] + } + } + }, + "Role": { + "description": "Represents the role of a participant in a conversation or message exchange.\n\nUsed in sampling and chat contexts to distinguish between different\ntypes of message senders in the conversation flow.", + "oneOf": [ + { + "description": "A human user or client making a request", + "type": "string", + "const": "user" + }, + { + "description": "An AI assistant or server providing a response", + "type": "string", + "const": "assistant" + } + ] + }, + "SamplingMessage": { + "description": "A message in a sampling conversation, containing a role and content.\n\nThis represents a single message in a conversation flow, used primarily\nin LLM sampling requests where the conversation history is important\nfor generating appropriate responses.", + "type": "object", + "properties": { + "content": { + "description": "The actual content of the message (text, image, etc.)", + "allOf": [ + { + "$ref": "#/definitions/Annotated" + } + ] + }, + "role": { + "description": "The role of the message sender (User or Assistant)", + "allOf": [ + { + "$ref": "#/definitions/Role" + } + ] + } + }, + "required": [ + "role", + "content" + ] + }, + "ServerCapabilities": { + "title": "Builder", + "description": "```rust\n# use rmcp::model::ServerCapabilities;\nlet cap = ServerCapabilities::builder()\n .enable_logging()\n .enable_experimental()\n .enable_prompts()\n .enable_resources()\n .enable_tools()\n .enable_tool_list_changed()\n .build();\n```", + "type": "object", + "properties": { + "completions": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "experimental": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "object", + "additionalProperties": true + } + }, + "logging": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "prompts": { + "anyOf": [ + { + "$ref": "#/definitions/PromptsCapability" + }, + { + "type": "null" + } + ] + }, + "resources": { + "anyOf": [ + { + "$ref": "#/definitions/ResourcesCapability" + }, + { + "type": "null" + } + ] + }, + "tools": { + "anyOf": [ + { + "$ref": "#/definitions/ToolsCapability" + }, + { + "type": "null" + } + ] + } + } + }, + "ServerResult": { + "anyOf": [ + { + "$ref": "#/definitions/InitializeResult" + }, + { + "$ref": "#/definitions/CompleteResult" + }, + { + "$ref": "#/definitions/GetPromptResult" + }, + { + "$ref": "#/definitions/ListPromptsResult" + }, + { + "$ref": "#/definitions/ListResourcesResult" + }, + { + "$ref": "#/definitions/ListResourceTemplatesResult" + }, + { + "$ref": "#/definitions/ReadResourceResult" + }, + { + "$ref": "#/definitions/CallToolResult" + }, + { + "$ref": "#/definitions/ListToolsResult" + }, + { + "$ref": "#/definitions/EmptyObject" + } + ] + }, + "Tool": { + "description": "A tool that can be used by a model.", + "type": "object", + "properties": { + "annotations": { + "description": "Optional additional tool information.", + "anyOf": [ + { + "$ref": "#/definitions/ToolAnnotations" + }, + { + "type": "null" + } + ] + }, + "description": { + "description": "A description of what the tool does", + "type": [ + "string", + "null" + ] + }, + "inputSchema": { + "description": "A JSON Schema object defining the expected parameters for the tool", + "type": "object", + "additionalProperties": true + }, + "name": { + "description": "The name of the tool", + "type": "string" + }, + "outputSchema": { + "description": "An optional JSON Schema object defining the structure of the tool's output", + "type": [ + "object", + "null" + ], + "additionalProperties": true + } + }, + "required": [ + "name", + "inputSchema" + ] + }, + "ToolAnnotations": { + "description": "Additional properties describing a Tool to clients.\n\nNOTE: all properties in ToolAnnotations are **hints**.\nThey are not guaranteed to provide a faithful description of\ntool behavior (including descriptive properties like `title`).\n\nClients should never make tool use decisions based on ToolAnnotations\nreceived from untrusted servers.", + "type": "object", + "properties": { + "destructiveHint": { + "description": "If true, the tool may perform destructive updates to its environment.\nIf false, the tool performs only additive updates.\n\n(This property is meaningful only when `readOnlyHint == false`)\n\nDefault: true\nA human-readable description of the tool's purpose.", + "type": [ + "boolean", + "null" + ] + }, + "idempotentHint": { + "description": "If true, calling the tool repeatedly with the same arguments\nwill have no additional effect on the its environment.\n\n(This property is meaningful only when `readOnlyHint == false`)\n\nDefault: false.", + "type": [ + "boolean", + "null" + ] + }, + "openWorldHint": { + "description": "If true, this tool may interact with an \"open world\" of external\nentities. If false, the tool's domain of interaction is closed.\nFor example, the world of a web search tool is open, whereas that\nof a memory tool is not.\n\nDefault: true", + "type": [ + "boolean", + "null" + ] + }, + "readOnlyHint": { + "description": "If true, the tool does not modify its environment.\n\nDefault: false", + "type": [ + "boolean", + "null" + ] + }, + "title": { + "description": "A human-readable title for the tool.", + "type": [ + "string", + "null" + ] + } + } + }, + "ToolListChangedNotificationMethod": { + "type": "string", + "format": "const", + "const": "notifications/tools/list_changed" + }, + "ToolsCapability": { + "type": "object", + "properties": { + "listChanged": { + "type": [ + "boolean", + "null" + ] + } + } + } + } +} \ No newline at end of file diff --git a/crates/rmcp/tests/test_structured_output.rs b/crates/rmcp/tests/test_structured_output.rs new file mode 100644 index 00000000..7e85d0e7 --- /dev/null +++ b/crates/rmcp/tests/test_structured_output.rs @@ -0,0 +1,233 @@ +//cargo test --test test_structured_output --features "client server macros" +use rmcp::{ + Json, ServerHandler, + handler::server::{router::tool::ToolRouter, tool::Parameters}, + model::{CallToolResult, Content, Tool}, + tool, tool_handler, tool_router, +}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use serde_json::json; + +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct CalculationRequest { + pub a: i32, + pub b: i32, +} + +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct CalculationResult { + pub sum: i32, + pub product: i32, +} + +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct UserInfo { + pub name: String, + pub age: u32, +} + +#[tool_handler(router = self.tool_router)] +impl ServerHandler for TestServer {} + +#[derive(Debug, Clone)] +pub struct TestServer { + tool_router: ToolRouter, +} + +impl Default for TestServer { + fn default() -> Self { + Self::new() + } +} + +#[tool_router(router = tool_router)] +impl TestServer { + pub fn new() -> Self { + Self { + tool_router: Self::tool_router(), + } + } + + /// Tool that returns structured output + #[tool(name = "calculate", description = "Perform calculations")] + pub async fn calculate( + &self, + params: Parameters, + ) -> Result, String> { + Ok(Json(CalculationResult { + sum: params.0.a + params.0.b, + product: params.0.a * params.0.b, + })) + } + + /// Tool that returns regular string output + #[tool(name = "get-greeting", description = "Get a greeting")] + pub async fn get_greeting(&self, name: Parameters) -> String { + format!("Hello, {}!", name.0) + } + + /// Tool that returns structured user info + #[tool(name = "get-user", description = "Get user info")] + pub async fn get_user(&self, user_id: Parameters) -> Result, String> { + if user_id.0 == "123" { + Ok(Json(UserInfo { + name: "Alice".to_string(), + age: 30, + })) + } else { + Err("User not found".to_string()) + } + } +} + +#[tokio::test] +async fn test_tool_with_output_schema() { + let server = TestServer::new(); + let tools = server.tool_router.list_all(); + + // Find the calculate tool + let calculate_tool = tools.iter().find(|t| t.name == "calculate").unwrap(); + + // Verify it has an output schema + assert!(calculate_tool.output_schema.is_some()); + + let schema = calculate_tool.output_schema.as_ref().unwrap(); + + // Check that the schema contains expected fields + let schema_str = serde_json::to_string(schema).unwrap(); + assert!(schema_str.contains("sum")); + assert!(schema_str.contains("product")); +} + +#[tokio::test] +async fn test_tool_without_output_schema() { + let server = TestServer::new(); + let tools = server.tool_router.list_all(); + + // Find the get-greeting tool + let greeting_tool = tools.iter().find(|t| t.name == "get-greeting").unwrap(); + + // Verify it doesn't have an output schema (returns String) + assert!(greeting_tool.output_schema.is_none()); +} + +#[tokio::test] +async fn test_structured_content_in_call_result() { + // Test creating a CallToolResult with structured content + let structured_data = json!({ + "sum": 7, + "product": 12 + }); + + let result = CallToolResult::structured(structured_data.clone()); + + assert!(result.content.is_none()); + assert!(result.structured_content.is_some()); + assert_eq!(result.structured_content.unwrap(), structured_data); + assert_eq!(result.is_error, Some(false)); +} + +#[tokio::test] +async fn test_structured_error_in_call_result() { + // Test creating a CallToolResult with structured error + let error_data = json!({ + "error_code": "NOT_FOUND", + "message": "User not found" + }); + + let result = CallToolResult::structured_error(error_data.clone()); + + assert!(result.content.is_none()); + assert!(result.structured_content.is_some()); + assert_eq!(result.structured_content.unwrap(), error_data); + assert_eq!(result.is_error, Some(true)); +} + +#[tokio::test] +async fn test_mutual_exclusivity_validation() { + // Test that content and structured_content are mutually exclusive + let content_result = CallToolResult::success(vec![Content::text("Hello")]); + let structured_result = CallToolResult::structured(json!({"message": "Hello"})); + + // Verify the validation + assert!(content_result.validate().is_ok()); + assert!(structured_result.validate().is_ok()); + + // Try to create an invalid result with both fields + let invalid_json = json!({ + "content": [{"type": "text", "text": "Hello"}], + "structuredContent": {"message": "Hello"} + }); + + // The deserialization itself should fail due to validation + let deserialized: Result = serde_json::from_value(invalid_json); + assert!(deserialized.is_err()); +} + +#[tokio::test] +async fn test_structured_return_conversion() { + // Test that Json converts to CallToolResult with structured_content + let calc_result = CalculationResult { + sum: 7, + product: 12, + }; + + let structured = Json(calc_result); + let result: Result = + rmcp::handler::server::tool::IntoCallToolResult::into_call_tool_result(structured); + + assert!(result.is_ok()); + let call_result = result.unwrap(); + + assert!(call_result.content.is_none()); + assert!(call_result.structured_content.is_some()); + + let structured_value = call_result.structured_content.unwrap(); + assert_eq!(structured_value["sum"], 7); + assert_eq!(structured_value["product"], 12); +} + +#[tokio::test] +async fn test_tool_serialization_with_output_schema() { + let server = TestServer::new(); + let tools = server.tool_router.list_all(); + + let calculate_tool = tools.iter().find(|t| t.name == "calculate").unwrap(); + + // Serialize the tool + let serialized = serde_json::to_value(calculate_tool).unwrap(); + + // Check that outputSchema is included + assert!(serialized["outputSchema"].is_object()); + + // Deserialize back + let deserialized: Tool = serde_json::from_value(serialized).unwrap(); + assert!(deserialized.output_schema.is_some()); +} + +#[tokio::test] +async fn test_output_schema_requires_structured_content() { + // Test that tools with output_schema must use structured_content + let server = TestServer::new(); + let tools = server.tool_router.list_all(); + + // The calculate tool should have output_schema + let calculate_tool = tools.iter().find(|t| t.name == "calculate").unwrap(); + assert!(calculate_tool.output_schema.is_some()); + + // Directly call the tool and verify its result structure + let params = rmcp::handler::server::tool::Parameters(CalculationRequest { a: 5, b: 3 }); + let result = server.calculate(params).await.unwrap(); + + // Convert the Json to CallToolResult + let call_result: Result = + rmcp::handler::server::tool::IntoCallToolResult::into_call_tool_result(result); + + assert!(call_result.is_ok()); + let call_result = call_result.unwrap(); + + // Verify it has structured_content and no content + assert!(call_result.structured_content.is_some()); + assert!(call_result.content.is_none()); +} diff --git a/crates/rmcp/tests/test_tool_builder_methods.rs b/crates/rmcp/tests/test_tool_builder_methods.rs new file mode 100644 index 00000000..f93c0546 --- /dev/null +++ b/crates/rmcp/tests/test_tool_builder_methods.rs @@ -0,0 +1,62 @@ +//cargo test --test test_tool_builder_methods --features "client server macros" +use rmcp::model::{JsonObject, Tool}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct InputData { + pub name: String, + pub age: u32, +} + +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct OutputData { + pub greeting: String, + pub is_adult: bool, +} + +#[test] +fn test_with_output_schema() { + let tool = Tool::new("test", "Test tool", JsonObject::new()).with_output_schema::(); + + assert!(tool.output_schema.is_some()); + + // Verify the schema contains expected fields + let schema_str = serde_json::to_string(tool.output_schema.as_ref().unwrap()).unwrap(); + assert!(schema_str.contains("greeting")); + assert!(schema_str.contains("is_adult")); +} + +#[test] +fn test_with_input_schema() { + let tool = Tool::new("test", "Test tool", JsonObject::new()).with_input_schema::(); + + // Verify the schema contains expected fields + let schema_str = serde_json::to_string(&tool.input_schema).unwrap(); + assert!(schema_str.contains("name")); + assert!(schema_str.contains("age")); +} + +#[test] +fn test_chained_builder_methods() { + let tool = Tool::new("test", "Test tool", JsonObject::new()) + .with_input_schema::() + .with_output_schema::() + .annotate(rmcp::model::ToolAnnotations::new().read_only(true)); + + assert!(tool.output_schema.is_some()); + assert!(tool.annotations.is_some()); + assert_eq!( + tool.annotations.as_ref().unwrap().read_only_hint, + Some(true) + ); + + // Verify both schemas are set correctly + let input_schema_str = serde_json::to_string(&tool.input_schema).unwrap(); + assert!(input_schema_str.contains("name")); + assert!(input_schema_str.contains("age")); + + let output_schema_str = serde_json::to_string(tool.output_schema.as_ref().unwrap()).unwrap(); + assert!(output_schema_str.contains("greeting")); + assert!(output_schema_str.contains("is_adult")); +} diff --git a/crates/rmcp/tests/test_tool_macros.rs b/crates/rmcp/tests/test_tool_macros.rs index b7631ed5..90791062 100644 --- a/crates/rmcp/tests/test_tool_macros.rs +++ b/crates/rmcp/tests/test_tool_macros.rs @@ -301,7 +301,8 @@ async fn test_optional_i64_field_with_null_input() -> anyhow::Result<()> { let result_text = result .content - .first() + .as_ref() + .and_then(|contents| contents.first()) .and_then(|content| content.raw.as_text()) .map(|text| text.text.as_str()) .expect("Expected text content"); @@ -329,7 +330,8 @@ async fn test_optional_i64_field_with_null_input() -> anyhow::Result<()> { let some_result_text = some_result .content - .first() + .as_ref() + .and_then(|contents| contents.first()) .and_then(|content| content.raw.as_text()) .map(|text| text.text.as_str()) .expect("Expected text content"); diff --git a/examples/servers/Cargo.toml b/examples/servers/Cargo.toml index b7d43c5d..58128541 100644 --- a/examples/servers/Cargo.toml +++ b/examples/servers/Cargo.toml @@ -9,6 +9,7 @@ publish = false [dependencies] rmcp = { workspace = true, features = [ "server", + "macros", "transport-sse-server", "transport-io", "transport-streamable-http-server", @@ -33,7 +34,7 @@ tracing-subscriber = { version = "0.3", features = [ futures = "0.3" rand = { version = "0.9", features = ["std"] } axum = { version = "0.8", features = ["macros"] } -schemars = { version = "1.0", optional = true } +schemars = { version = "1.0" } reqwest = { version = "0.12", features = ["json"] } chrono = "0.4" uuid = { version = "1.6", features = ["v4", "serde"] } @@ -82,3 +83,7 @@ path = "src/counter_hyper_streamable_http.rs" [[example]] name = "servers_sampling_stdio" path = "src/sampling_stdio.rs" + +[[example]] +name = "servers_structured_output" +path = "src/structured_output.rs" diff --git a/examples/servers/src/sampling_stdio.rs b/examples/servers/src/sampling_stdio.rs index 28d2b79c..dec242d4 100644 --- a/examples/servers/src/sampling_stdio.rs +++ b/examples/servers/src/sampling_stdio.rs @@ -119,6 +119,7 @@ impl ServerHandler for SamplingDemoServer { })) .unwrap(), ), + output_schema: None, annotations: None, }], next_cursor: None, diff --git a/examples/servers/src/structured_output.rs b/examples/servers/src/structured_output.rs new file mode 100644 index 00000000..f39a9619 --- /dev/null +++ b/examples/servers/src/structured_output.rs @@ -0,0 +1,158 @@ +//! Example demonstrating structured output from tools +//! +//! This example shows how to: +//! - Return structured data from tools using the Json wrapper +//! - Automatically generate output schemas from Rust types +//! - Handle both structured and unstructured tool outputs + +use rmcp::{ + Json, ServiceExt, + handler::server::{router::tool::ToolRouter, tool::Parameters}, + tool, tool_handler, tool_router, + transport::stdio, +}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct WeatherRequest { + pub city: String, + pub units: Option, +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct WeatherResponse { + pub temperature: f64, + pub description: String, + pub humidity: u8, + pub wind_speed: f64, +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct CalculationRequest { + pub numbers: Vec, + pub operation: String, +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct CalculationResult { + pub result: f64, + pub operation: String, + pub input_count: usize, +} + +#[derive(Clone)] +pub struct StructuredOutputServer { + tool_router: ToolRouter, +} + +#[tool_handler(router = self.tool_router)] +impl rmcp::ServerHandler for StructuredOutputServer {} + +impl Default for StructuredOutputServer { + fn default() -> Self { + Self::new() + } +} + +#[tool_router(router = tool_router)] +impl StructuredOutputServer { + pub fn new() -> Self { + Self { + tool_router: Self::tool_router(), + } + } + + /// Get weather information for a city (returns structured data) + #[tool(name = "get_weather", description = "Get current weather for a city")] + pub async fn get_weather( + &self, + params: Parameters, + ) -> Result, String> { + // Simulate weather API call + let weather = WeatherResponse { + temperature: match params.0.units.as_deref() { + Some("fahrenheit") => 72.5, + _ => 22.5, // celsius by default + }, + description: "Partly cloudy".to_string(), + humidity: 65, + wind_speed: 12.5, + }; + + Ok(Json(weather)) + } + + /// Perform calculations on a list of numbers (returns structured data) + #[tool(name = "calculate", description = "Perform calculations on numbers")] + pub async fn calculate( + &self, + params: Parameters, + ) -> Result, String> { + let numbers = ¶ms.0.numbers; + if numbers.is_empty() { + return Err("No numbers provided".to_string()); + } + + let result = match params.0.operation.as_str() { + "sum" => numbers.iter().sum::() as f64, + "average" => numbers.iter().sum::() as f64 / numbers.len() as f64, + "product" => numbers.iter().product::() as f64, + _ => return Err(format!("Unknown operation: {}", params.0.operation)), + }; + + Ok(Json(CalculationResult { + result, + operation: params.0.operation, + input_count: numbers.len(), + })) + } + + /// Get server info (returns unstructured text) + #[tool(name = "get_info", description = "Get server information")] + pub async fn get_info(&self) -> String { + "Structured Output Example Server v1.0".to_string() + } +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + eprintln!("Starting structured output example server..."); + eprintln!(); + eprintln!("This server demonstrates:"); + eprintln!("- Tools that return structured JSON data"); + eprintln!("- Automatic output schema generation"); + eprintln!("- Mixed structured and unstructured outputs"); + eprintln!(); + eprintln!("Tools available:"); + eprintln!("- get_weather: Returns structured weather data"); + eprintln!("- calculate: Returns structured calculation results"); + eprintln!("- get_info: Returns plain text"); + eprintln!(); + + let server = StructuredOutputServer::new(); + + // Print the tools with their schemas for demonstration + eprintln!("Tool schemas:"); + for tool in server.tool_router.list_all() { + eprintln!("\n{}: {}", tool.name, tool.description.unwrap_or_default()); + if let Some(output_schema) = &tool.output_schema { + eprintln!( + " Output schema: {}", + serde_json::to_string_pretty(output_schema).unwrap() + ); + } else { + eprintln!(" Output: Unstructured text"); + } + } + eprintln!(); + + // Start the server + eprintln!("Starting server. Connect with an MCP client to test the tools."); + eprintln!("Press Ctrl+C to stop."); + + let service = server.serve(stdio()).await?; + service.waiting().await?; + + Ok(()) +} diff --git a/examples/simple-chat-client/src/chat.rs b/examples/simple-chat-client/src/chat.rs index 221389ed..abbe0d2e 100644 --- a/examples/simple-chat-client/src/chat.rs +++ b/examples/simple-chat-client/src/chat.rs @@ -85,8 +85,8 @@ impl ChatSession { if result.is_error.is_some_and(|b| b) { self.messages .push(Message::user("tool call failed, mcp call error")); - } else { - result.content.iter().for_each(|content| { + } else if let Some(contents) = &result.content { + contents.iter().for_each(|content| { if let Some(content_text) = content.as_text() { let json_result = serde_json::from_str::( &content_text.text,