diff --git a/crates/rmcp-macros/src/prompt.rs b/crates/rmcp-macros/src/prompt.rs index cb3d7442..d5b0b0f1 100644 --- a/crates/rmcp-macros/src/prompt.rs +++ b/crates/rmcp-macros/src/prompt.rs @@ -18,6 +18,8 @@ pub struct PromptAttribute { pub arguments: Option, /// Optional icons for the prompt pub icons: Option, + /// Optional metadata for the prompt + pub meta: Option, } pub struct ResolvedPromptAttribute { @@ -26,6 +28,7 @@ pub struct ResolvedPromptAttribute { pub description: Option, pub arguments: Expr, pub icons: Option, + pub meta: Option, } impl ResolvedPromptAttribute { @@ -36,6 +39,7 @@ impl ResolvedPromptAttribute { arguments, title, icons, + meta, } = self; let description = if let Some(description) = description { quote! { Some(#description.into()) } @@ -52,6 +56,11 @@ impl ResolvedPromptAttribute { } else { quote! { None } }; + let meta = if let Some(meta) = meta { + quote! { Some(#meta) } + } else { + quote! { None } + }; let tokens = quote! { pub fn #fn_ident() -> rmcp::model::Prompt { rmcp::model::Prompt { @@ -60,6 +69,7 @@ impl ResolvedPromptAttribute { arguments: #arguments, title: #title, icons: #icons, + meta: #meta, } } }; @@ -114,6 +124,7 @@ pub fn prompt(attr: TokenStream, input: TokenStream) -> syn::Result arguments: arguments.clone(), title: attribute.title, icons: attribute.icons, + meta: attribute.meta, }; let prompt_attr_fn = resolved_prompt_attr.into_fn(prompt_attr_fn_ident.clone())?; diff --git a/crates/rmcp-macros/src/prompt_handler.rs b/crates/rmcp-macros/src/prompt_handler.rs index f6f46d6f..c37f1eab 100644 --- a/crates/rmcp-macros/src/prompt_handler.rs +++ b/crates/rmcp-macros/src/prompt_handler.rs @@ -7,6 +7,7 @@ use syn::{Expr, ImplItem, ItemImpl, parse_quote}; #[darling(default)] pub struct PromptHandlerAttribute { pub router: Option, + pub meta: Option, } pub fn prompt_handler(attr: TokenStream, input: TokenStream) -> syn::Result { @@ -40,6 +41,12 @@ pub fn prompt_handler(attr: TokenStream, input: TokenStream) -> syn::Result syn::Result, } impl Default for ToolHandlerAttribute { @@ -16,13 +17,14 @@ impl Default for ToolHandlerAttribute { self.tool_router }) .unwrap(), + meta: None, } } } pub fn tool_handler(attr: TokenStream, input: TokenStream) -> syn::Result { let attr_args = NestedMeta::parse_meta_list(attr)?; - let ToolHandlerAttribute { router } = ToolHandlerAttribute::from_list(&attr_args)?; + let ToolHandlerAttribute { router, meta } = ToolHandlerAttribute::from_list(&attr_args)?; let mut item_impl = syn::parse2::(input.clone())?; let tool_call_fn = quote! { async fn call_tool( @@ -34,13 +36,24 @@ pub fn tool_handler(attr: TokenStream, input: TokenStream) -> syn::Result, _context: rmcp::service::RequestContext, ) -> Result { - Ok(rmcp::model::ListToolsResult::with_all_items(#router.list_all())) + Ok(rmcp::model::ListToolsResult{ + tools: #router.list_all(), + meta: #result_meta, + next_cursor: None, + }) } }; let tool_call_fn = syn::parse2::(tool_call_fn)?; diff --git a/crates/rmcp/src/handler/server/router.rs b/crates/rmcp/src/handler/server/router.rs index 23b15dde..0b908081 100644 --- a/crates/rmcp/src/handler/server/router.rs +++ b/crates/rmcp/src/handler/server/router.rs @@ -102,7 +102,7 @@ where let tools = self.tool_router.list_all(); Ok(ServerResult::ListToolsResult(ListToolsResult { tools, - next_cursor: None, + ..Default::default() })) } ClientRequest::GetPromptRequest(request) => { @@ -125,7 +125,7 @@ where let prompts = self.prompt_router.list_all(); Ok(ServerResult::ListPromptsResult(ListPromptsResult { prompts, - next_cursor: None, + ..Default::default() })) } rest => self.service.handle_request(rest, context).await, diff --git a/crates/rmcp/src/model.rs b/crates/rmcp/src/model.rs index fb757c09..9ef04f0d 100644 --- a/crates/rmcp/src/model.rs +++ b/crates/rmcp/src/model.rs @@ -794,6 +794,8 @@ macro_rules! paginated_result { #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct $t { + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option, #[serde(skip_serializing_if = "Option::is_none")] pub next_cursor: Option, pub $i_item: $t_item, @@ -804,6 +806,7 @@ macro_rules! paginated_result { items: $t_item, ) -> Self { Self { + meta: None, next_cursor: None, $i_item: items, } diff --git a/crates/rmcp/src/model/content.rs b/crates/rmcp/src/model/content.rs index 7c60fafd..fb82053d 100644 --- a/crates/rmcp/src/model/content.rs +++ b/crates/rmcp/src/model/content.rs @@ -258,6 +258,7 @@ mod tests { mime_type: Some("text/plain".to_string()), size: Some(100), icons: None, + meta: None, }); let json = serde_json::to_string(&resource_link).unwrap(); diff --git a/crates/rmcp/src/model/prompt.rs b/crates/rmcp/src/model/prompt.rs index 3c69758c..bb63938f 100644 --- a/crates/rmcp/src/model/prompt.rs +++ b/crates/rmcp/src/model/prompt.rs @@ -2,7 +2,7 @@ use base64::engine::{Engine, general_purpose::STANDARD as BASE64_STANDARD}; use serde::{Deserialize, Serialize}; use super::{ - AnnotateAble, Annotations, Icon, RawEmbeddedResource, RawImageContent, + AnnotateAble, Annotations, Icon, Meta, RawEmbeddedResource, RawImageContent, content::{EmbeddedResource, ImageContent}, resource::ResourceContents, }; @@ -25,6 +25,9 @@ pub struct Prompt { /// Optional list of icons for the prompt #[serde(skip_serializing_if = "Option::is_none")] pub icons: Option>, + /// Optional additional metadata for this prompt + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option, } impl Prompt { @@ -44,6 +47,7 @@ impl Prompt { description: description.map(Into::into), arguments, icons: None, + meta: None, } } } diff --git a/crates/rmcp/src/model/resource.rs b/crates/rmcp/src/model/resource.rs index a342ad4e..a542d173 100644 --- a/crates/rmcp/src/model/resource.rs +++ b/crates/rmcp/src/model/resource.rs @@ -29,6 +29,9 @@ pub struct RawResource { /// Optional list of icons for the resource #[serde(skip_serializing_if = "Option::is_none")] pub icons: Option>, + /// Optional additional metadata for this resource + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option, } pub type Resource = Annotated; @@ -95,6 +98,7 @@ impl RawResource { mime_type: None, size: None, icons: None, + meta: None, } } } @@ -115,6 +119,7 @@ mod tests { mime_type: Some("text/plain".to_string()), size: Some(100), icons: None, + meta: None, }; let json = serde_json::to_string(&resource).unwrap(); 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 8fa565c0..44bfb210 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 @@ -976,6 +976,14 @@ "description": "Represents a resource in the extension with metadata", "type": "object", "properties": { + "_meta": { + "description": "Optional additional metadata for this resource", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "description": { "description": "Optional description of the resource", "type": [ diff --git a/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema_current.json b/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema_current.json index 8fa565c0..44bfb210 100644 --- a/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema_current.json +++ b/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema_current.json @@ -976,6 +976,14 @@ "description": "Represents a resource in the extension with metadata", "type": "object", "properties": { + "_meta": { + "description": "Optional additional metadata for this resource", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "description": { "description": "Optional description of the resource", "type": [ 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 223989cb..abee9651 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 @@ -172,6 +172,14 @@ "description": "Represents a resource in the extension with metadata", "type": "object", "properties": { + "_meta": { + "description": "Optional additional metadata for this resource", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "annotations": { "anyOf": [ { @@ -1031,6 +1039,13 @@ "ListPromptsResult": { "type": "object", "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "nextCursor": { "type": [ "string", @@ -1051,6 +1066,13 @@ "ListResourceTemplatesResult": { "type": "object", "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "nextCursor": { "type": [ "string", @@ -1071,6 +1093,13 @@ "ListResourcesResult": { "type": "object", "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "nextCursor": { "type": [ "string", @@ -1096,6 +1125,13 @@ "ListToolsResult": { "type": "object", "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "nextCursor": { "type": [ "string", @@ -1472,6 +1508,14 @@ "description": "A prompt that can be used to generate text from a model", "type": "object", "properties": { + "_meta": { + "description": "Optional additional metadata for this prompt", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "arguments": { "description": "Optional arguments that can be passed to customize the prompt", "type": [ @@ -1660,6 +1704,14 @@ "description": "A link to a resource that can be fetched separately", "type": "object", "properties": { + "_meta": { + "description": "Optional additional metadata for this resource", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "annotations": { "anyOf": [ { @@ -1816,6 +1868,14 @@ "description": "Represents a resource in the extension with metadata", "type": "object", "properties": { + "_meta": { + "description": "Optional additional metadata for this resource", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "description": { "description": "Optional description of the resource", "type": [ 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 index 223989cb..abee9651 100644 --- 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 @@ -172,6 +172,14 @@ "description": "Represents a resource in the extension with metadata", "type": "object", "properties": { + "_meta": { + "description": "Optional additional metadata for this resource", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "annotations": { "anyOf": [ { @@ -1031,6 +1039,13 @@ "ListPromptsResult": { "type": "object", "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "nextCursor": { "type": [ "string", @@ -1051,6 +1066,13 @@ "ListResourceTemplatesResult": { "type": "object", "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "nextCursor": { "type": [ "string", @@ -1071,6 +1093,13 @@ "ListResourcesResult": { "type": "object", "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "nextCursor": { "type": [ "string", @@ -1096,6 +1125,13 @@ "ListToolsResult": { "type": "object", "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "nextCursor": { "type": [ "string", @@ -1472,6 +1508,14 @@ "description": "A prompt that can be used to generate text from a model", "type": "object", "properties": { + "_meta": { + "description": "Optional additional metadata for this prompt", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "arguments": { "description": "Optional arguments that can be passed to customize the prompt", "type": [ @@ -1660,6 +1704,14 @@ "description": "A link to a resource that can be fetched separately", "type": "object", "properties": { + "_meta": { + "description": "Optional additional metadata for this resource", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "annotations": { "anyOf": [ { @@ -1816,6 +1868,14 @@ "description": "Represents a resource in the extension with metadata", "type": "object", "properties": { + "_meta": { + "description": "Optional additional metadata for this resource", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "description": { "description": "Optional description of the resource", "type": [ diff --git a/examples/servers/src/common/counter.rs b/examples/servers/src/common/counter.rs index 13b236ab..0c2bd4aa 100644 --- a/examples/servers/src/common/counter.rs +++ b/examples/servers/src/common/counter.rs @@ -30,7 +30,7 @@ pub struct ExamplePromptArgs { #[derive(Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)] pub struct CounterAnalysisArgs { /// The target value you're trying to reach - pub goal: i32, + pub goal: String, /// Preferred strategy: 'fast' or 'careful' #[serde(skip_serializing_if = "Option::is_none")] pub strategy: Option, @@ -110,7 +110,10 @@ impl Counter { #[prompt_router] impl Counter { /// This is an example prompt that takes one required argument, message - #[prompt(name = "example_prompt")] + #[prompt( + name = "example_prompt", + meta = Meta(rmcp::object!({"meta_key": "meta_value"})) + )] async fn example_prompt( &self, Parameters(args): Parameters, @@ -135,7 +138,12 @@ impl Counter { ) -> Result { let strategy = args.strategy.unwrap_or_else(|| "careful".to_string()); let current_value = *self.counter.lock().await; - let difference = args.goal - current_value; + let difference = args.goal.parse::().map_err(|e| { + McpError::invalid_request( + "invalid_goal", + Some(json!(format!("Goal must be a valid integer: {}", e))), + ) + })? - current_value; let messages = vec![ PromptMessage::new_text( @@ -161,8 +169,8 @@ impl Counter { } } -#[tool_handler] -#[prompt_handler] +#[tool_handler(meta = Meta(rmcp::object!({"tool_meta_key": "tool_meta_value"})))] +#[prompt_handler(meta = Meta(rmcp::object!({"router_meta_key": "router_meta_value"})))] impl ServerHandler for Counter { fn get_info(&self) -> ServerInfo { ServerInfo { @@ -188,6 +196,7 @@ impl ServerHandler for Counter { self._create_resource_text("memo://insights", "memo-name"), ], next_cursor: None, + meta: None, }) } @@ -226,6 +235,7 @@ impl ServerHandler for Counter { Ok(ListResourceTemplatesResult { next_cursor: None, resource_templates: Vec::new(), + meta: None, }) }