diff --git a/crates/rmcp-macros/src/prompt.rs b/crates/rmcp-macros/src/prompt.rs index 128e48ebc..68a147f65 100644 --- a/crates/rmcp-macros/src/prompt.rs +++ b/crates/rmcp-macros/src/prompt.rs @@ -10,6 +10,8 @@ use crate::common::{extract_doc_line, none_expr}; pub struct PromptAttribute { /// The name of the prompt pub name: Option, + /// Human readable title of prompt + pub title: Option, /// Optional description of what the prompt does pub description: Option, /// Arguments that can be passed to the prompt @@ -18,6 +20,7 @@ pub struct PromptAttribute { pub struct ResolvedPromptAttribute { pub name: String, + pub title: Option, pub description: Option, pub arguments: Expr, } @@ -28,18 +31,25 @@ impl ResolvedPromptAttribute { name, description, arguments, + title, } = self; let description = if let Some(description) = description { quote! { Some(#description.into()) } } else { quote! { None } }; + let title = if let Some(title) = title { + quote! { Some(#title.into()) } + } else { + quote! { None } + }; let tokens = quote! { pub fn #fn_ident() -> rmcp::model::Prompt { rmcp::model::Prompt { name: #name.into(), description: #description, arguments: #arguments, + title: #title, } } }; @@ -87,6 +97,7 @@ pub fn prompt(attr: TokenStream, input: TokenStream) -> syn::Result name: name.clone(), description: description.clone(), arguments: arguments.clone(), + title: attribute.title, }; let prompt_attr_fn = resolved_prompt_attr.into_fn(prompt_attr_fn_ident.clone())?; diff --git a/crates/rmcp-macros/src/tool.rs b/crates/rmcp-macros/src/tool.rs index d6662f079..a9c6e0d96 100644 --- a/crates/rmcp-macros/src/tool.rs +++ b/crates/rmcp-macros/src/tool.rs @@ -66,6 +66,8 @@ fn extract_schema_from_return_type(ret_type: &syn::Type) -> Option { pub struct ToolAttribute { /// The name of the tool pub name: Option, + /// Human readable title of tool + pub title: Option, pub description: Option, /// A JSON Schema object defining the expected parameters for the tool pub input_schema: Option, @@ -77,6 +79,7 @@ pub struct ToolAttribute { pub struct ResolvedToolAttribute { pub name: String, + pub title: Option, pub description: Option, pub input_schema: Expr, pub output_schema: Option, @@ -88,6 +91,7 @@ impl ResolvedToolAttribute { let Self { name, description, + title, input_schema, output_schema, annotations, @@ -102,10 +106,16 @@ impl ResolvedToolAttribute { } else { quote! { None } }; + let title = if let Some(title) = title { + quote! { Some(#title.into()) } + } else { + quote! { None } + }; let tokens = quote! { pub fn #fn_ident() -> rmcp::model::Tool { rmcp::model::Tool { name: #name.into(), + title: #title, description: #description, input_schema: #input_schema, output_schema: #output_schema, @@ -229,6 +239,7 @@ pub fn tool(attr: TokenStream, input: TokenStream) -> syn::Result { input_schema: input_schema_expr, output_schema: output_schema_expr, annotations: annotations_expr, + title: attribute.title, }; let tool_attr_fn = resolved_tool_attr.into_fn(tool_attr_fn_ident)?; // modify the the input function diff --git a/crates/rmcp/src/handler/client.rs b/crates/rmcp/src/handler/client.rs index 92e5fa293..147f2fc29 100644 --- a/crates/rmcp/src/handler/client.rs +++ b/crates/rmcp/src/handler/client.rs @@ -16,6 +16,7 @@ impl Service for H { ServerRequest::CreateMessageRequest(request) => self .create_message(request.params, context) .await + .map(Box::new) .map(ClientResult::CreateMessageResult), ServerRequest::ListRootsRequest(_) => self .list_roots(context) diff --git a/crates/rmcp/src/handler/server/prompt.rs b/crates/rmcp/src/handler/server/prompt.rs index c512d380e..5c262e2b7 100644 --- a/crates/rmcp/src/handler/server/prompt.rs +++ b/crates/rmcp/src/handler/server/prompt.rs @@ -350,6 +350,7 @@ pub fn cached_arguments_from_schema() arguments.push(crate::model::PromptArgument { name: name.clone(), + title: None, description, required: Some(required.contains(name.as_str())), }); diff --git a/crates/rmcp/src/model.rs b/crates/rmcp/src/model.rs index 45764f803..3c09f7a73 100644 --- a/crates/rmcp/src/model.rs +++ b/crates/rmcp/src/model.rs @@ -493,7 +493,7 @@ impl ErrorData { /// Represents any JSON-RPC message that can be sent or received. /// /// This enum covers all possible message types in the JSON-RPC protocol: -/// individual requests/responses, notifications, batch operations, and errors. +/// individual requests/responses, notifications, and errors. /// It serves as the top-level message container for MCP communication. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(untagged)] @@ -686,6 +686,8 @@ impl Default for ClientInfo { #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct Implementation { pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, pub version: String, } @@ -699,6 +701,7 @@ impl Implementation { pub fn from_build_env() -> Self { Implementation { name: env!("CARGO_CRATE_NAME").to_owned(), + title: None, version: env!("CARGO_PKG_VERSION").to_owned(), } } @@ -1104,6 +1107,8 @@ pub struct ResourceReference { #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct PromptReference { pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, } const_string!(CompleteRequestMethod = "completion/complete"); @@ -1438,24 +1443,50 @@ pub struct GetPromptResult { macro_rules! ts_union { ( - export type $U: ident = - $(|)?$($V: ident)|*; + export type $U:ident = + $($rest:tt)* ) => { + ts_union!(@declare $U { $($rest)* }); + ts_union!(@impl_from $U { $($rest)* }); + }; + (@declare $U:ident { $($variant:tt)* }) => { + ts_union!(@declare_variant $U { } {$($variant)*} ); + }; + (@declare_variant $U:ident { $($declared:tt)* } {$(|)? box $V:ident $($rest:tt)*}) => { + ts_union!(@declare_variant $U { $($declared)* $V(Box<$V>), } {$($rest)*}); + }; + (@declare_variant $U:ident { $($declared:tt)* } {$(|)? $V:ident $($rest:tt)*}) => { + ts_union!(@declare_variant $U { $($declared)* $V($V), } {$($rest)*}); + }; + (@declare_variant $U:ident { $($declared:tt)* } { ; }) => { + ts_union!(@declare_end $U { $($declared)* } ); + }; + (@declare_end $U:ident { $($declared:tt)* }) => { #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(untagged)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum $U { - $($V($V),)* + $($declared)* } - - $( - impl From<$V> for $U { - fn from(value: $V) -> Self { - $U::$V(value) - } + }; + (@impl_from $U: ident {$(|)? box $V:ident $($rest:tt)*}) => { + impl From<$V> for $U { + fn from(value: $V) -> Self { + $U::$V(Box::new(value)) } - )* + } + ts_union!(@impl_from $U {$($rest)*}); + }; + (@impl_from $U: ident {$(|)? $V:ident $($rest:tt)*}) => { + impl From<$V> for $U { + fn from(value: $V) -> Self { + $U::$V(value) + } + } + ts_union!(@impl_from $U {$($rest)*}); }; + (@impl_from $U: ident { ; }) => {}; + (@impl_from $U: ident { }) => {}; } ts_union!( @@ -1504,7 +1535,7 @@ ts_union!( ); ts_union!( - export type ClientResult = CreateMessageResult | ListRootsResult | CreateElicitationResult | EmptyResult; + export type ClientResult = box CreateMessageResult | ListRootsResult | CreateElicitationResult | EmptyResult; ); impl ClientResult { diff --git a/crates/rmcp/src/model/content.rs b/crates/rmcp/src/model/content.rs index dfc90ed12..e5e7fd198 100644 --- a/crates/rmcp/src/model/content.rs +++ b/crates/rmcp/src/model/content.rs @@ -59,7 +59,7 @@ pub enum RawContent { Text(RawTextContent), Image(RawImageContent), Resource(RawEmbeddedResource), - Audio(AudioContent), + Audio(RawAudioContent), ResourceLink(super::resource::RawResource), } @@ -241,6 +241,7 @@ mod tests { let resource_link = RawContent::ResourceLink(RawResource { uri: "file:///test.txt".to_string(), name: "test.txt".to_string(), + title: None, description: Some("A test file".to_string()), mime_type: Some("text/plain".to_string()), size: Some(100), diff --git a/crates/rmcp/src/model/prompt.rs b/crates/rmcp/src/model/prompt.rs index 7b8eff337..bb90c97cb 100644 --- a/crates/rmcp/src/model/prompt.rs +++ b/crates/rmcp/src/model/prompt.rs @@ -14,6 +14,8 @@ use super::{ pub struct Prompt { /// The name of the prompt pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, /// Optional description of what the prompt does #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, @@ -35,6 +37,7 @@ impl Prompt { { Prompt { name: name.into(), + title: None, description: description.map(Into::into), arguments, } @@ -47,6 +50,9 @@ impl Prompt { pub struct PromptArgument { /// The name of the argument pub name: String, + /// A human-readable title for the argument + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, /// A description of what the argument is used for #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, diff --git a/crates/rmcp/src/model/resource.rs b/crates/rmcp/src/model/resource.rs index 3e9923db6..12b4fbc0e 100644 --- a/crates/rmcp/src/model/resource.rs +++ b/crates/rmcp/src/model/resource.rs @@ -11,6 +11,9 @@ pub struct RawResource { pub uri: String, /// Name of the resource pub name: String, + /// Human-readable title of the resource + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, /// Optional description of the resource #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, @@ -34,6 +37,8 @@ pub struct RawResourceTemplate { pub uri_template: String, pub name: String, #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, #[serde(skip_serializing_if = "Option::is_none")] pub mime_type: Option, @@ -82,6 +87,7 @@ impl RawResource { Self { uri: uri.into(), name: name.into(), + title: None, description: None, mime_type: None, size: None, @@ -99,6 +105,7 @@ mod tests { fn test_resource_serialization() { let resource = RawResource { uri: "file:///test.txt".to_string(), + title: None, name: "test".to_string(), description: Some("Test resource".to_string()), mime_type: Some("text/plain".to_string()), diff --git a/crates/rmcp/src/model/tool.rs b/crates/rmcp/src/model/tool.rs index b2ea4bf8f..225534fbc 100644 --- a/crates/rmcp/src/model/tool.rs +++ b/crates/rmcp/src/model/tool.rs @@ -15,6 +15,9 @@ use super::JsonObject; pub struct Tool { /// The name of the tool pub name: Cow<'static, str>, + /// A human-readable title for the tool + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, /// A description of what the tool does #[serde(skip_serializing_if = "Option::is_none")] pub description: Option>, @@ -138,6 +141,7 @@ impl Tool { { Tool { name: name.into(), + title: None, description: Some(description.into()), input_schema: input_schema.into(), output_schema: None, diff --git a/crates/rmcp/src/service/server.rs b/crates/rmcp/src/service/server.rs index 95655f6a2..513196d38 100644 --- a/crates/rmcp/src/service/server.rs +++ b/crates/rmcp/src/service/server.rs @@ -384,7 +384,22 @@ macro_rules! method { } impl Peer { - method!(peer_req create_message CreateMessageRequest(CreateMessageRequestParam) => CreateMessageResult); + pub async fn create_message( + &self, + params: CreateMessageRequestParam, + ) -> Result { + let result = self + .send_request(ServerRequest::CreateMessageRequest(CreateMessageRequest { + method: Default::default(), + params, + extensions: Default::default(), + })) + .await?; + match result { + ClientResult::CreateMessageResult(result) => Ok(*result), + _ => Err(ServiceError::UnexpectedResponse), + } + } method!(peer_req list_roots ListRootsRequest() => ListRootsResult); #[cfg(feature = "elicitation")] method!(peer_req create_elicitation CreateElicitationRequest(CreateElicitationRequestParam) => CreateElicitationResult); diff --git a/crates/rmcp/tests/test_elicitation.rs b/crates/rmcp/tests/test_elicitation.rs index 81ceeb1fa..3b3ea01ac 100644 --- a/crates/rmcp/tests/test_elicitation.rs +++ b/crates/rmcp/tests/test_elicitation.rs @@ -892,6 +892,7 @@ async fn test_initialize_request_with_elicitation() { client_info: Implementation { name: "test-client".to_string(), version: "1.0.0".to_string(), + title: None, }, }; @@ -933,6 +934,7 @@ async fn test_capability_checking_logic() { client_info: Implementation { name: "test-client".to_string(), version: "1.0.0".to_string(), + title: None, }, }; @@ -950,6 +952,7 @@ async fn test_capability_checking_logic() { client_info: Implementation { name: "test-client".to_string(), version: "1.0.0".to_string(), + title: None, }, }; diff --git a/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json b/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json index 0fefb1238..a1d5a0b36 100644 --- a/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json +++ b/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json @@ -1,7 +1,7 @@ { "$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.", + "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, and errors.\nIt serves as the top-level message container for MCP communication.", "anyOf": [ { "description": "A single request expecting a response", @@ -113,7 +113,7 @@ }, "allOf": [ { - "$ref": "#/definitions/Annotated2" + "$ref": "#/definitions/RawAudioContent" } ], "required": [ @@ -139,31 +139,6 @@ } ] }, - "Annotated2": { - "type": "object", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType" - ] - }, "Annotations": { "type": "object", "properties": { @@ -491,6 +466,12 @@ "name": { "type": "string" }, + "title": { + "type": [ + "string", + "null" + ] + }, "version": { "type": "string" } @@ -845,6 +826,12 @@ "properties": { "name": { "type": "string" + }, + "title": { + "type": [ + "string", + "null" + ] } }, "required": [ @@ -855,6 +842,21 @@ "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" }, + "RawAudioContent": { + "type": "object", + "properties": { + "data": { + "type": "string" + }, + "mimeType": { + "type": "string" + } + }, + "required": [ + "data", + "mimeType" + ] + }, "RawEmbeddedResource": { "type": "object", "properties": { @@ -920,6 +922,13 @@ "format": "uint32", "minimum": 0 }, + "title": { + "description": "Human-readable title of the resource", + "type": [ + "string", + "null" + ] + }, "uri": { "description": "URI representing the resource location (e.g., \"file:///path/to/file\" or \"str:///content\")", "type": "string" 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 5c1bc0d2a..e118fe19c 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 @@ -1,7 +1,7 @@ { "$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.", + "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, and errors.\nIt serves as the top-level message container for MCP communication.", "anyOf": [ { "description": "A single request expecting a response", @@ -113,7 +113,7 @@ }, "allOf": [ { - "$ref": "#/definitions/Annotated2" + "$ref": "#/definitions/RawAudioContent" } ], "required": [ @@ -140,31 +140,6 @@ ] }, "Annotated2": { - "type": "object", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType" - ] - }, - "Annotated3": { "type": "object", "properties": { "_meta": { @@ -192,7 +167,7 @@ "resource" ] }, - "Annotated4": { + "Annotated3": { "description": "Represents a resource in the extension with metadata", "type": "object", "properties": { @@ -233,6 +208,13 @@ "format": "uint32", "minimum": 0 }, + "title": { + "description": "Human-readable title of the resource", + "type": [ + "string", + "null" + ] + }, "uri": { "description": "URI representing the resource location (e.g., \"file:///path/to/file\" or \"str:///content\")", "type": "string" @@ -243,7 +225,7 @@ "name" ] }, - "Annotated5": { + "Annotated4": { "type": "object", "properties": { "annotations": { @@ -271,6 +253,12 @@ "name": { "type": "string" }, + "title": { + "type": [ + "string", + "null" + ] + }, "uriTemplate": { "type": "string" } @@ -625,6 +613,12 @@ "name": { "type": "string" }, + "title": { + "type": [ + "string", + "null" + ] + }, "version": { "type": "string" } @@ -814,7 +808,7 @@ "resourceTemplates": { "type": "array", "items": { - "$ref": "#/definitions/Annotated5" + "$ref": "#/definitions/Annotated4" } } }, @@ -834,7 +828,7 @@ "resources": { "type": "array", "items": { - "$ref": "#/definitions/Annotated4" + "$ref": "#/definitions/Annotated3" } } }, @@ -1144,6 +1138,12 @@ "name": { "description": "The name of the prompt", "type": "string" + }, + "title": { + "type": [ + "string", + "null" + ] } }, "required": [ @@ -1171,6 +1171,13 @@ "boolean", "null" ] + }, + "title": { + "description": "A human-readable title for the argument", + "type": [ + "string", + "null" + ] } }, "required": [ @@ -1265,7 +1272,7 @@ "type": "object", "properties": { "resource": { - "$ref": "#/definitions/Annotated3" + "$ref": "#/definitions/Annotated2" }, "type": { "type": "string", @@ -1318,6 +1325,13 @@ "format": "uint32", "minimum": 0 }, + "title": { + "description": "Human-readable title of the resource", + "type": [ + "string", + "null" + ] + }, "type": { "type": "string", "const": "resource_link" @@ -1358,6 +1372,21 @@ "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" }, + "RawAudioContent": { + "type": "object", + "properties": { + "data": { + "type": "string" + }, + "mimeType": { + "type": "string" + } + }, + "required": [ + "data", + "mimeType" + ] + }, "RawEmbeddedResource": { "type": "object", "properties": { @@ -1423,6 +1452,13 @@ "format": "uint32", "minimum": 0 }, + "title": { + "description": "Human-readable title of the resource", + "type": [ + "string", + "null" + ] + }, "uri": { "description": "URI representing the resource location (e.g., \"file:///path/to/file\" or \"str:///content\")", "type": "string" @@ -1791,6 +1827,13 @@ "null" ], "additionalProperties": true + }, + "title": { + "description": "A human-readable title for the tool", + "type": [ + "string", + "null" + ] } }, "required": [ diff --git a/examples/clients/src/sse.rs b/examples/clients/src/sse.rs index df7948790..aaff4f48f 100644 --- a/examples/clients/src/sse.rs +++ b/examples/clients/src/sse.rs @@ -22,6 +22,7 @@ async fn main() -> Result<()> { capabilities: ClientCapabilities::default(), client_info: Implementation { name: "test sse client".to_string(), + title: None, version: "0.0.1".to_string(), }, }; diff --git a/examples/clients/src/streamable_http.rs b/examples/clients/src/streamable_http.rs index 634ec61b8..1fef3c6ea 100644 --- a/examples/clients/src/streamable_http.rs +++ b/examples/clients/src/streamable_http.rs @@ -22,6 +22,7 @@ async fn main() -> Result<()> { capabilities: ClientCapabilities::default(), client_info: Implementation { name: "test sse client".to_string(), + title: None, version: "0.0.1".to_string(), }, }; diff --git a/examples/servers/src/sampling_stdio.rs b/examples/servers/src/sampling_stdio.rs index dec242d4c..3afec818c 100644 --- a/examples/servers/src/sampling_stdio.rs +++ b/examples/servers/src/sampling_stdio.rs @@ -105,6 +105,7 @@ impl ServerHandler for SamplingDemoServer { Ok(ListToolsResult { tools: vec![Tool { name: "ask_llm".into(), + title: None, description: Some("Ask a question to the LLM through sampling".into()), input_schema: Arc::new( serde_json::from_value(serde_json::json!({