From 4cab8a26e036373c49ec233aff4f6963b6ce3e25 Mon Sep 17 00:00:00 2001 From: Dale Seo <5466341+DaleSeo@users.noreply.github.com> Date: Tue, 3 Mar 2026 11:11:27 -0500 Subject: [PATCH 1/8] fix: builder with_* methods take T instead of Option --- crates/rmcp/src/model/prompt.rs | 12 ++++++------ crates/rmcp/src/model/tool.rs | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/crates/rmcp/src/model/prompt.rs b/crates/rmcp/src/model/prompt.rs index 4d491d0e..531a86d2 100644 --- a/crates/rmcp/src/model/prompt.rs +++ b/crates/rmcp/src/model/prompt.rs @@ -68,20 +68,20 @@ impl Prompt { } /// Set the human-readable title - pub fn with_title(mut self, title: Option) -> Self { - self.title = title; + pub fn with_title(mut self, title: impl Into) -> Self { + self.title = Some(title.into()); self } /// Set the icons - pub fn with_icons(mut self, icons: Option>) -> Self { - self.icons = icons; + pub fn with_icons(mut self, icons: Vec) -> Self { + self.icons = Some(icons); self } /// Set the metadata - pub fn with_meta(mut self, meta: Option) -> Self { - self.meta = meta; + pub fn with_meta(mut self, meta: Meta) -> Self { + self.meta = Some(meta); self } } diff --git a/crates/rmcp/src/model/tool.rs b/crates/rmcp/src/model/tool.rs index 82b762de..ca6e5691 100644 --- a/crates/rmcp/src/model/tool.rs +++ b/crates/rmcp/src/model/tool.rs @@ -261,32 +261,32 @@ impl Tool { } /// Set the human-readable title - pub fn with_title(mut self, title: Option) -> Self { - self.title = title; + pub fn with_title(mut self, title: impl Into) -> Self { + self.title = Some(title.into()); self } /// Set the output schema from a raw value - pub fn with_raw_output_schema(mut self, output_schema: Option>) -> Self { - self.output_schema = output_schema; + pub fn with_raw_output_schema(mut self, output_schema: Arc) -> Self { + self.output_schema = Some(output_schema); self } /// Set the annotations - pub fn with_annotations(mut self, annotations: Option) -> Self { - self.annotations = annotations; + pub fn with_annotations(mut self, annotations: ToolAnnotations) -> Self { + self.annotations = Some(annotations); self } /// Set the icons - pub fn with_icons(mut self, icons: Option>) -> Self { - self.icons = icons; + pub fn with_icons(mut self, icons: Vec) -> Self { + self.icons = Some(icons); self } /// Set the metadata - pub fn with_meta(mut self, meta: Option) -> Self { - self.meta = meta; + pub fn with_meta(mut self, meta: Meta) -> Self { + self.meta = Some(meta); self } @@ -298,8 +298,8 @@ impl Tool { } /// Set the execution configuration for this tool. - pub fn with_execution(mut self, execution: Option) -> Self { - self.execution = execution; + pub fn with_execution(mut self, execution: ToolExecution) -> Self { + self.execution = Some(execution); self } From b3ea79515013e8ba115938aec0161d093e12c5b7 Mon Sep 17 00:00:00 2001 From: Dale Seo <5466341+DaleSeo@users.noreply.github.com> Date: Tue, 3 Mar 2026 11:11:31 -0500 Subject: [PATCH 2/8] fix: emit conditional builder calls for optional fields in macros --- crates/rmcp-macros/src/prompt.rs | 28 +++++++----------- crates/rmcp-macros/src/tool.rs | 49 ++++++++++++++------------------ 2 files changed, 32 insertions(+), 45 deletions(-) diff --git a/crates/rmcp-macros/src/prompt.rs b/crates/rmcp-macros/src/prompt.rs index a7a13f45..20492a66 100644 --- a/crates/rmcp-macros/src/prompt.rs +++ b/crates/rmcp-macros/src/prompt.rs @@ -46,21 +46,13 @@ impl ResolvedPromptAttribute { } else { quote! { None:: } }; - let title = if let Some(title) = title { - quote! { Some(#title.into()) } - } else { - quote! { None } - }; - let icons = if let Some(icons) = icons { - quote! { Some(#icons) } - } else { - quote! { None } - }; - let meta = if let Some(meta) = meta { - quote! { Some(#meta) } - } else { - quote! { None } - }; + let title_call = title + .map(|t| quote! { .with_title(#t) }) + .unwrap_or_default(); + let icons_call = icons + .map(|i| quote! { .with_icons(#i) }) + .unwrap_or_default(); + let meta_call = meta.map(|m| quote! { .with_meta(#m) }).unwrap_or_default(); let tokens = quote! { pub fn #fn_ident() -> rmcp::model::Prompt { rmcp::model::Prompt::from_raw( @@ -68,9 +60,9 @@ impl ResolvedPromptAttribute { #description, #arguments, ) - .with_title(#title) - .with_icons(#icons) - .with_meta(#meta) + #title_call + #icons_call + #meta_call } }; syn::parse2::(tokens) diff --git a/crates/rmcp-macros/src/tool.rs b/crates/rmcp-macros/src/tool.rs index 0e36a889..24330902 100644 --- a/crates/rmcp-macros/src/tool.rs +++ b/crates/rmcp-macros/src/tool.rs @@ -134,42 +134,37 @@ impl ResolvedToolAttribute { } else { quote! { None } }; - let output_schema = if let Some(output_schema) = output_schema { - quote! { Some(#output_schema) } - } else { - quote! { None } - }; - let title = if let Some(title) = title { - quote! { Some(#title.into()) } - } else { - quote! { None } - }; - let icons = if let Some(icons) = icons { - quote! { Some(#icons) } - } else { - quote! { None } - }; - let meta = if let Some(meta) = meta { - quote! { Some(#meta) } - } else { - quote! { None } - }; + let title_call = title + .map(|t| quote! { .with_title(#t) }) + .unwrap_or_default(); + let output_schema_call = output_schema + .map(|s| quote! { .with_raw_output_schema(#s) }) + .unwrap_or_default(); + let icons_call = icons + .map(|i| quote! { .with_icons(#i) }) + .unwrap_or_default(); + let meta_call = meta.map(|m| quote! { .with_meta(#m) }).unwrap_or_default(); let doc_comment = format!("Generated tool metadata function for {name}"); let doc_attr: syn::Attribute = parse_quote!(#[doc = #doc_comment]); let tokens = quote! { #doc_attr pub fn #fn_ident() -> rmcp::model::Tool { - rmcp::model::Tool::new_with_raw( + let mut __tool = rmcp::model::Tool::new_with_raw( #name, #description, #input_schema, ) - .with_title(#title) - .with_raw_output_schema(#output_schema) - .with_annotations(#annotations) - .with_execution(#execution) - .with_icons(#icons) - .with_meta(#meta) + #title_call + #output_schema_call + #icons_call + #meta_call; + if let Some(__annotations) = #annotations { + __tool = __tool.with_annotations(__annotations); + } + if let Some(__execution) = #execution { + __tool = __tool.with_execution(__execution); + } + __tool } }; syn::parse2::(tokens) From 9ea54fbef234a9803d9dc19ffc9c09915eb7c212 Mon Sep 17 00:00:00 2001 From: Dale Seo <5466341+DaleSeo@users.noreply.github.com> Date: Tue, 3 Mar 2026 11:11:36 -0500 Subject: [PATCH 3/8] fix: convert with_task, with_stop_reason, with_logger, with_content to proper builders --- crates/rmcp/src/model.rs | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/crates/rmcp/src/model.rs b/crates/rmcp/src/model.rs index c0b3dc43..53806151 100644 --- a/crates/rmcp/src/model.rs +++ b/crates/rmcp/src/model.rs @@ -1455,13 +1455,10 @@ impl LoggingMessageNotificationParam { } } - /// Create with a logger name. - pub fn with_logger(level: LoggingLevel, logger: impl Into, data: Value) -> Self { - Self { - level, - logger: Some(logger.into()), - data, - } + /// Set the logger name. + pub fn with_logger(mut self, logger: impl Into) -> Self { + self.logger = Some(logger.into()); + self } } @@ -2605,12 +2602,10 @@ impl CreateElicitationResult { } } - /// Create with content. - pub fn with_content(action: ElicitationAction, content: Value) -> Self { - Self { - action, - content: Some(content), - } + /// Set the content on this result. + pub fn with_content(mut self, content: Value) -> Self { + self.content = Some(content); + self } } @@ -2822,8 +2817,8 @@ impl CallToolRequestParams { } /// Sets the task metadata for this tool call. - pub fn with_task(mut self, task: Option) -> Self { - self.task = task; + pub fn with_task(mut self, task: JsonObject) -> Self { + self.task = Some(task); self } } @@ -2889,8 +2884,8 @@ impl CreateMessageResult { pub const STOP_REASON_TOOL_USE: &str = "toolUse"; /// Set the stop reason. - pub fn with_stop_reason(mut self, stop_reason: Option) -> Self { - self.stop_reason = stop_reason; + pub fn with_stop_reason(mut self, stop_reason: impl Into) -> Self { + self.stop_reason = Some(stop_reason.into()); self } From bd9a3d9148610be9f84d4ffa71f2a50c7b8e26ff Mon Sep 17 00:00:00 2001 From: Dale Seo <5466341+DaleSeo@users.noreply.github.com> Date: Tue, 3 Mar 2026 11:11:40 -0500 Subject: [PATCH 4/8] fix: update test callers for new builder signatures --- crates/rmcp/tests/common/handlers.rs | 2 +- crates/rmcp/tests/test_sampling.rs | 8 ++++---- crates/rmcp/tests/test_task_support_validation.rs | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/rmcp/tests/common/handlers.rs b/crates/rmcp/tests/common/handlers.rs index 2084981a..811bd824 100644 --- a/crates/rmcp/tests/common/handlers.rs +++ b/crates/rmcp/tests/common/handlers.rs @@ -78,7 +78,7 @@ impl ClientHandler for TestClientHandler { SamplingMessage::assistant_text(response.to_string()), "test-model".to_string(), ) - .with_stop_reason(Some(CreateMessageResult::STOP_REASON_END_TURN.to_string()))) + .with_stop_reason(CreateMessageResult::STOP_REASON_END_TURN)) } fn on_logging_message( diff --git a/crates/rmcp/tests/test_sampling.rs b/crates/rmcp/tests/test_sampling.rs index d885e46c..02da06cf 100644 --- a/crates/rmcp/tests/test_sampling.rs +++ b/crates/rmcp/tests/test_sampling.rs @@ -55,7 +55,7 @@ async fn test_sampling_result_structure() -> Result<()> { SamplingMessage::assistant_text("The capital of France is Paris."), "test-model".to_string(), ) - .with_stop_reason(Some(CreateMessageResult::STOP_REASON_END_TURN.to_string())); + .with_stop_reason(CreateMessageResult::STOP_REASON_END_TURN); let json = serde_json::to_string(&result)?; let deserialized: CreateMessageResult = serde_json::from_str(&json)?; @@ -436,7 +436,7 @@ async fn test_create_message_result_tool_use_stop_reason() -> Result<()> { ), "test-model".to_string(), ) - .with_stop_reason(Some(CreateMessageResult::STOP_REASON_TOOL_USE.to_string())); + .with_stop_reason(CreateMessageResult::STOP_REASON_TOOL_USE); let json = serde_json::to_string(&result)?; let deserialized: CreateMessageResult = serde_json::from_str(&json)?; @@ -688,7 +688,7 @@ async fn test_create_message_result_validate_rejects_user_role() { SamplingMessage::user_text("This should not be a user message"), "test-model".to_string(), ) - .with_stop_reason(Some(CreateMessageResult::STOP_REASON_END_TURN.to_string())); + .with_stop_reason(CreateMessageResult::STOP_REASON_END_TURN); let err = result.validate().unwrap_err(); assert!( @@ -703,7 +703,7 @@ async fn test_create_message_result_validate_accepts_assistant_role() { SamplingMessage::assistant_text("Hello!"), "test-model".to_string(), ) - .with_stop_reason(Some(CreateMessageResult::STOP_REASON_END_TURN.to_string())); + .with_stop_reason(CreateMessageResult::STOP_REASON_END_TURN); assert!(result.validate().is_ok()); } diff --git a/crates/rmcp/tests/test_task_support_validation.rs b/crates/rmcp/tests/test_task_support_validation.rs index cd999768..a8626c59 100644 --- a/crates/rmcp/tests/test_task_support_validation.rs +++ b/crates/rmcp/tests/test_task_support_validation.rs @@ -75,8 +75,8 @@ impl ClientHandler for DummyClientHandler { } /// Helper to create a task object for tool calls -fn make_task() -> Option { - Some(json!({}).as_object().unwrap().clone()) +fn make_task() -> JsonObject { + json!({}).as_object().unwrap().clone() } #[tokio::test] From 454c605ed43194e45402e3b18acf24209161c953 Mon Sep 17 00:00:00 2001 From: Dale Seo <5466341+DaleSeo@users.noreply.github.com> Date: Tue, 3 Mar 2026 11:20:52 -0500 Subject: [PATCH 5/8] fix: simplify make_task helper and remove unused import --- crates/rmcp/tests/test_task_support_validation.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/rmcp/tests/test_task_support_validation.rs b/crates/rmcp/tests/test_task_support_validation.rs index a8626c59..88d2ed51 100644 --- a/crates/rmcp/tests/test_task_support_validation.rs +++ b/crates/rmcp/tests/test_task_support_validation.rs @@ -13,7 +13,6 @@ use rmcp::{ model::{CallToolRequestParams, ClientInfo, ErrorCode, JsonObject}, tool, tool_handler, tool_router, }; -use serde_json::json; /// Server with tools having different task support modes. #[derive(Debug, Clone)] @@ -76,7 +75,7 @@ impl ClientHandler for DummyClientHandler { /// Helper to create a task object for tool calls fn make_task() -> JsonObject { - json!({}).as_object().unwrap().clone() + serde_json::Map::new() } #[tokio::test] From dedb226a68d512af9c748b41075f81cee17845da Mon Sep 17 00:00:00 2001 From: Dale Seo <5466341+DaleSeo@users.noreply.github.com> Date: Tue, 3 Mar 2026 11:37:59 -0500 Subject: [PATCH 6/8] fix: update sampling_stdio example for new with_stop_reason signature --- examples/clients/src/sampling_stdio.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/clients/src/sampling_stdio.rs b/examples/clients/src/sampling_stdio.rs index 27b9273c..cc7c5f15 100644 --- a/examples/clients/src/sampling_stdio.rs +++ b/examples/clients/src/sampling_stdio.rs @@ -44,7 +44,7 @@ impl ClientHandler for SamplingDemoClient { SamplingMessage::assistant_text(response_text), "mock_llm".to_string(), ) - .with_stop_reason(Some(CreateMessageResult::STOP_REASON_END_TURN.to_string()))) + .with_stop_reason(CreateMessageResult::STOP_REASON_END_TURN)) } } From 99ec0d3502417e7a275c3ae69c1a5b225c28d732 Mon Sep 17 00:00:00 2001 From: Dale Seo <5466341+DaleSeo@users.noreply.github.com> Date: Tue, 3 Mar 2026 11:45:44 -0500 Subject: [PATCH 7/8] fix: make annotations and execution Option consistent with other fields --- crates/rmcp-macros/src/tool.rs | 39 +++++++++++++++++----------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/crates/rmcp-macros/src/tool.rs b/crates/rmcp-macros/src/tool.rs index 24330902..d0a124d8 100644 --- a/crates/rmcp-macros/src/tool.rs +++ b/crates/rmcp-macros/src/tool.rs @@ -110,8 +110,8 @@ pub struct ResolvedToolAttribute { pub description: Option, pub input_schema: Expr, pub output_schema: Option, - pub annotations: Expr, - pub execution: Expr, + pub annotations: Option, + pub execution: Option, pub icons: Option, pub meta: Option, } @@ -140,6 +140,12 @@ impl ResolvedToolAttribute { let output_schema_call = output_schema .map(|s| quote! { .with_raw_output_schema(#s) }) .unwrap_or_default(); + let annotations_call = annotations + .map(|a| quote! { .with_annotations(#a) }) + .unwrap_or_default(); + let execution_call = execution + .map(|e| quote! { .with_execution(#e) }) + .unwrap_or_default(); let icons_call = icons .map(|i| quote! { .with_icons(#i) }) .unwrap_or_default(); @@ -149,22 +155,17 @@ impl ResolvedToolAttribute { let tokens = quote! { #doc_attr pub fn #fn_ident() -> rmcp::model::Tool { - let mut __tool = rmcp::model::Tool::new_with_raw( + rmcp::model::Tool::new_with_raw( #name, #description, #input_schema, ) #title_call #output_schema_call + #annotations_call + #execution_call #icons_call - #meta_call; - if let Some(__annotations) = #annotations { - __tool = __tool.with_annotations(__annotations); - } - if let Some(__execution) = #execution { - __tool = __tool.with_execution(__execution); - } - __tool + #meta_call } }; syn::parse2::(tokens) @@ -255,17 +256,17 @@ pub fn tool(attr: TokenStream, input: TokenStream) -> syn::Result { let idempotent_hint = wrap_option(idempotent_hint); let open_world_hint = wrap_option(open_world_hint); let token_stream = quote! { - Some(rmcp::model::ToolAnnotations::from_raw( + rmcp::model::ToolAnnotations::from_raw( #title, #read_only_hint, #destructive_hint, #idempotent_hint, #open_world_hint, - )) + ) }; - syn::parse2::(token_stream)? + Some(syn::parse2::(token_stream)?) } else { - none_expr()? + None }; let execution_expr = if let Some(execution) = attribute.execution { let ToolExecutionAttribute { task_support } = execution; @@ -291,13 +292,13 @@ pub fn tool(attr: TokenStream, input: TokenStream) -> syn::Result { }; let token_stream = quote! { - Some(rmcp::model::ToolExecution::from_raw( + rmcp::model::ToolExecution::from_raw( #task_support_expr, - )) + ) }; - syn::parse2::(token_stream)? + Some(syn::parse2::(token_stream)?) } else { - none_expr()? + None }; // Handle output_schema - either explicit or generated from return type let output_schema_expr = attribute.output_schema.or_else(|| { From 34f9f33f7c94211c3c10740fe5dc20119b8aad4b Mon Sep 17 00:00:00 2001 From: Dale Seo <5466341+DaleSeo@users.noreply.github.com> Date: Tue, 3 Mar 2026 11:48:44 -0500 Subject: [PATCH 8/8] fix: remove unused none_expr import --- crates/rmcp-macros/src/tool.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rmcp-macros/src/tool.rs b/crates/rmcp-macros/src/tool.rs index d0a124d8..56bf65a1 100644 --- a/crates/rmcp-macros/src/tool.rs +++ b/crates/rmcp-macros/src/tool.rs @@ -3,7 +3,7 @@ use proc_macro2::{Span, TokenStream}; use quote::{ToTokens, format_ident, quote}; use syn::{Expr, Ident, ImplItemFn, LitStr, ReturnType, parse_quote}; -use crate::common::{extract_doc_line, none_expr}; +use crate::common::extract_doc_line; /// Check if a type is Json and extract the inner type T fn extract_json_inner_type(ty: &syn::Type) -> Option<&syn::Type> {