fix: api ergonomics follow-up (#720)
* fix: builder with_* methods take T instead of Option<T> * fix: emit conditional builder calls for optional fields in macros * fix: convert with_task, with_stop_reason, with_logger, with_content to proper builders * fix: update test callers for new builder signatures * fix: simplify make_task helper and remove unused import * fix: update sampling_stdio example for new with_stop_reason signature * fix: make annotations and execution Option<Expr> consistent with other fields * fix: remove unused none_expr import
This commit is contained in:
parent
1fe5d1e1cd
commit
2d90b76501
9 changed files with 81 additions and 99 deletions
|
|
@ -46,21 +46,13 @@ impl ResolvedPromptAttribute {
|
||||||
} else {
|
} else {
|
||||||
quote! { None::<String> }
|
quote! { None::<String> }
|
||||||
};
|
};
|
||||||
let title = if let Some(title) = title {
|
let title_call = title
|
||||||
quote! { Some(#title.into()) }
|
.map(|t| quote! { .with_title(#t) })
|
||||||
} else {
|
.unwrap_or_default();
|
||||||
quote! { None }
|
let icons_call = icons
|
||||||
};
|
.map(|i| quote! { .with_icons(#i) })
|
||||||
let icons = if let Some(icons) = icons {
|
.unwrap_or_default();
|
||||||
quote! { Some(#icons) }
|
let meta_call = meta.map(|m| quote! { .with_meta(#m) }).unwrap_or_default();
|
||||||
} else {
|
|
||||||
quote! { None }
|
|
||||||
};
|
|
||||||
let meta = if let Some(meta) = meta {
|
|
||||||
quote! { Some(#meta) }
|
|
||||||
} else {
|
|
||||||
quote! { None }
|
|
||||||
};
|
|
||||||
let tokens = quote! {
|
let tokens = quote! {
|
||||||
pub fn #fn_ident() -> rmcp::model::Prompt {
|
pub fn #fn_ident() -> rmcp::model::Prompt {
|
||||||
rmcp::model::Prompt::from_raw(
|
rmcp::model::Prompt::from_raw(
|
||||||
|
|
@ -68,9 +60,9 @@ impl ResolvedPromptAttribute {
|
||||||
#description,
|
#description,
|
||||||
#arguments,
|
#arguments,
|
||||||
)
|
)
|
||||||
.with_title(#title)
|
#title_call
|
||||||
.with_icons(#icons)
|
#icons_call
|
||||||
.with_meta(#meta)
|
#meta_call
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
syn::parse2::<ImplItemFn>(tokens)
|
syn::parse2::<ImplItemFn>(tokens)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use proc_macro2::{Span, TokenStream};
|
||||||
use quote::{ToTokens, format_ident, quote};
|
use quote::{ToTokens, format_ident, quote};
|
||||||
use syn::{Expr, Ident, ImplItemFn, LitStr, ReturnType, parse_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<T> and extract the inner type T
|
/// Check if a type is Json<T> and extract the inner type T
|
||||||
fn extract_json_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
|
fn extract_json_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
|
||||||
|
|
@ -110,8 +110,8 @@ pub struct ResolvedToolAttribute {
|
||||||
pub description: Option<Expr>,
|
pub description: Option<Expr>,
|
||||||
pub input_schema: Expr,
|
pub input_schema: Expr,
|
||||||
pub output_schema: Option<Expr>,
|
pub output_schema: Option<Expr>,
|
||||||
pub annotations: Expr,
|
pub annotations: Option<Expr>,
|
||||||
pub execution: Expr,
|
pub execution: Option<Expr>,
|
||||||
pub icons: Option<Expr>,
|
pub icons: Option<Expr>,
|
||||||
pub meta: Option<Expr>,
|
pub meta: Option<Expr>,
|
||||||
}
|
}
|
||||||
|
|
@ -134,26 +134,22 @@ impl ResolvedToolAttribute {
|
||||||
} else {
|
} else {
|
||||||
quote! { None }
|
quote! { None }
|
||||||
};
|
};
|
||||||
let output_schema = if let Some(output_schema) = output_schema {
|
let title_call = title
|
||||||
quote! { Some(#output_schema) }
|
.map(|t| quote! { .with_title(#t) })
|
||||||
} else {
|
.unwrap_or_default();
|
||||||
quote! { None }
|
let output_schema_call = output_schema
|
||||||
};
|
.map(|s| quote! { .with_raw_output_schema(#s) })
|
||||||
let title = if let Some(title) = title {
|
.unwrap_or_default();
|
||||||
quote! { Some(#title.into()) }
|
let annotations_call = annotations
|
||||||
} else {
|
.map(|a| quote! { .with_annotations(#a) })
|
||||||
quote! { None }
|
.unwrap_or_default();
|
||||||
};
|
let execution_call = execution
|
||||||
let icons = if let Some(icons) = icons {
|
.map(|e| quote! { .with_execution(#e) })
|
||||||
quote! { Some(#icons) }
|
.unwrap_or_default();
|
||||||
} else {
|
let icons_call = icons
|
||||||
quote! { None }
|
.map(|i| quote! { .with_icons(#i) })
|
||||||
};
|
.unwrap_or_default();
|
||||||
let meta = if let Some(meta) = meta {
|
let meta_call = meta.map(|m| quote! { .with_meta(#m) }).unwrap_or_default();
|
||||||
quote! { Some(#meta) }
|
|
||||||
} else {
|
|
||||||
quote! { None }
|
|
||||||
};
|
|
||||||
let doc_comment = format!("Generated tool metadata function for {name}");
|
let doc_comment = format!("Generated tool metadata function for {name}");
|
||||||
let doc_attr: syn::Attribute = parse_quote!(#[doc = #doc_comment]);
|
let doc_attr: syn::Attribute = parse_quote!(#[doc = #doc_comment]);
|
||||||
let tokens = quote! {
|
let tokens = quote! {
|
||||||
|
|
@ -164,12 +160,12 @@ impl ResolvedToolAttribute {
|
||||||
#description,
|
#description,
|
||||||
#input_schema,
|
#input_schema,
|
||||||
)
|
)
|
||||||
.with_title(#title)
|
#title_call
|
||||||
.with_raw_output_schema(#output_schema)
|
#output_schema_call
|
||||||
.with_annotations(#annotations)
|
#annotations_call
|
||||||
.with_execution(#execution)
|
#execution_call
|
||||||
.with_icons(#icons)
|
#icons_call
|
||||||
.with_meta(#meta)
|
#meta_call
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
syn::parse2::<ImplItemFn>(tokens)
|
syn::parse2::<ImplItemFn>(tokens)
|
||||||
|
|
@ -260,17 +256,17 @@ pub fn tool(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream> {
|
||||||
let idempotent_hint = wrap_option(idempotent_hint);
|
let idempotent_hint = wrap_option(idempotent_hint);
|
||||||
let open_world_hint = wrap_option(open_world_hint);
|
let open_world_hint = wrap_option(open_world_hint);
|
||||||
let token_stream = quote! {
|
let token_stream = quote! {
|
||||||
Some(rmcp::model::ToolAnnotations::from_raw(
|
rmcp::model::ToolAnnotations::from_raw(
|
||||||
#title,
|
#title,
|
||||||
#read_only_hint,
|
#read_only_hint,
|
||||||
#destructive_hint,
|
#destructive_hint,
|
||||||
#idempotent_hint,
|
#idempotent_hint,
|
||||||
#open_world_hint,
|
#open_world_hint,
|
||||||
))
|
)
|
||||||
};
|
};
|
||||||
syn::parse2::<Expr>(token_stream)?
|
Some(syn::parse2::<Expr>(token_stream)?)
|
||||||
} else {
|
} else {
|
||||||
none_expr()?
|
None
|
||||||
};
|
};
|
||||||
let execution_expr = if let Some(execution) = attribute.execution {
|
let execution_expr = if let Some(execution) = attribute.execution {
|
||||||
let ToolExecutionAttribute { task_support } = execution;
|
let ToolExecutionAttribute { task_support } = execution;
|
||||||
|
|
@ -296,13 +292,13 @@ pub fn tool(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream> {
|
||||||
};
|
};
|
||||||
|
|
||||||
let token_stream = quote! {
|
let token_stream = quote! {
|
||||||
Some(rmcp::model::ToolExecution::from_raw(
|
rmcp::model::ToolExecution::from_raw(
|
||||||
#task_support_expr,
|
#task_support_expr,
|
||||||
))
|
)
|
||||||
};
|
};
|
||||||
syn::parse2::<Expr>(token_stream)?
|
Some(syn::parse2::<Expr>(token_stream)?)
|
||||||
} else {
|
} else {
|
||||||
none_expr()?
|
None
|
||||||
};
|
};
|
||||||
// Handle output_schema - either explicit or generated from return type
|
// Handle output_schema - either explicit or generated from return type
|
||||||
let output_schema_expr = attribute.output_schema.or_else(|| {
|
let output_schema_expr = attribute.output_schema.or_else(|| {
|
||||||
|
|
|
||||||
|
|
@ -1455,13 +1455,10 @@ impl LoggingMessageNotificationParam {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create with a logger name.
|
/// Set the logger name.
|
||||||
pub fn with_logger(level: LoggingLevel, logger: impl Into<String>, data: Value) -> Self {
|
pub fn with_logger(mut self, logger: impl Into<String>) -> Self {
|
||||||
Self {
|
self.logger = Some(logger.into());
|
||||||
level,
|
self
|
||||||
logger: Some(logger.into()),
|
|
||||||
data,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2605,12 +2602,10 @@ impl CreateElicitationResult {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create with content.
|
/// Set the content on this result.
|
||||||
pub fn with_content(action: ElicitationAction, content: Value) -> Self {
|
pub fn with_content(mut self, content: Value) -> Self {
|
||||||
Self {
|
self.content = Some(content);
|
||||||
action,
|
self
|
||||||
content: Some(content),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2822,8 +2817,8 @@ impl CallToolRequestParams {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the task metadata for this tool call.
|
/// Sets the task metadata for this tool call.
|
||||||
pub fn with_task(mut self, task: Option<JsonObject>) -> Self {
|
pub fn with_task(mut self, task: JsonObject) -> Self {
|
||||||
self.task = task;
|
self.task = Some(task);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2889,8 +2884,8 @@ impl CreateMessageResult {
|
||||||
pub const STOP_REASON_TOOL_USE: &str = "toolUse";
|
pub const STOP_REASON_TOOL_USE: &str = "toolUse";
|
||||||
|
|
||||||
/// Set the stop reason.
|
/// Set the stop reason.
|
||||||
pub fn with_stop_reason(mut self, stop_reason: Option<String>) -> Self {
|
pub fn with_stop_reason(mut self, stop_reason: impl Into<String>) -> Self {
|
||||||
self.stop_reason = stop_reason;
|
self.stop_reason = Some(stop_reason.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,20 +68,20 @@ impl Prompt {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the human-readable title
|
/// Set the human-readable title
|
||||||
pub fn with_title(mut self, title: Option<String>) -> Self {
|
pub fn with_title(mut self, title: impl Into<String>) -> Self {
|
||||||
self.title = title;
|
self.title = Some(title.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the icons
|
/// Set the icons
|
||||||
pub fn with_icons(mut self, icons: Option<Vec<Icon>>) -> Self {
|
pub fn with_icons(mut self, icons: Vec<Icon>) -> Self {
|
||||||
self.icons = icons;
|
self.icons = Some(icons);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the metadata
|
/// Set the metadata
|
||||||
pub fn with_meta(mut self, meta: Option<Meta>) -> Self {
|
pub fn with_meta(mut self, meta: Meta) -> Self {
|
||||||
self.meta = meta;
|
self.meta = Some(meta);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -261,32 +261,32 @@ impl Tool {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the human-readable title
|
/// Set the human-readable title
|
||||||
pub fn with_title(mut self, title: Option<String>) -> Self {
|
pub fn with_title(mut self, title: impl Into<String>) -> Self {
|
||||||
self.title = title;
|
self.title = Some(title.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the output schema from a raw value
|
/// Set the output schema from a raw value
|
||||||
pub fn with_raw_output_schema(mut self, output_schema: Option<Arc<JsonObject>>) -> Self {
|
pub fn with_raw_output_schema(mut self, output_schema: Arc<JsonObject>) -> Self {
|
||||||
self.output_schema = output_schema;
|
self.output_schema = Some(output_schema);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the annotations
|
/// Set the annotations
|
||||||
pub fn with_annotations(mut self, annotations: Option<ToolAnnotations>) -> Self {
|
pub fn with_annotations(mut self, annotations: ToolAnnotations) -> Self {
|
||||||
self.annotations = annotations;
|
self.annotations = Some(annotations);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the icons
|
/// Set the icons
|
||||||
pub fn with_icons(mut self, icons: Option<Vec<Icon>>) -> Self {
|
pub fn with_icons(mut self, icons: Vec<Icon>) -> Self {
|
||||||
self.icons = icons;
|
self.icons = Some(icons);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the metadata
|
/// Set the metadata
|
||||||
pub fn with_meta(mut self, meta: Option<Meta>) -> Self {
|
pub fn with_meta(mut self, meta: Meta) -> Self {
|
||||||
self.meta = meta;
|
self.meta = Some(meta);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -298,8 +298,8 @@ impl Tool {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the execution configuration for this tool.
|
/// Set the execution configuration for this tool.
|
||||||
pub fn with_execution(mut self, execution: Option<ToolExecution>) -> Self {
|
pub fn with_execution(mut self, execution: ToolExecution) -> Self {
|
||||||
self.execution = execution;
|
self.execution = Some(execution);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ impl ClientHandler for TestClientHandler {
|
||||||
SamplingMessage::assistant_text(response.to_string()),
|
SamplingMessage::assistant_text(response.to_string()),
|
||||||
"test-model".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(
|
fn on_logging_message(
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ async fn test_sampling_result_structure() -> Result<()> {
|
||||||
SamplingMessage::assistant_text("The capital of France is Paris."),
|
SamplingMessage::assistant_text("The capital of France is Paris."),
|
||||||
"test-model".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);
|
||||||
|
|
||||||
let json = serde_json::to_string(&result)?;
|
let json = serde_json::to_string(&result)?;
|
||||||
let deserialized: CreateMessageResult = serde_json::from_str(&json)?;
|
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(),
|
"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 json = serde_json::to_string(&result)?;
|
||||||
let deserialized: CreateMessageResult = serde_json::from_str(&json)?;
|
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"),
|
SamplingMessage::user_text("This should not be a user message"),
|
||||||
"test-model".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);
|
||||||
|
|
||||||
let err = result.validate().unwrap_err();
|
let err = result.validate().unwrap_err();
|
||||||
assert!(
|
assert!(
|
||||||
|
|
@ -703,7 +703,7 @@ async fn test_create_message_result_validate_accepts_assistant_role() {
|
||||||
SamplingMessage::assistant_text("Hello!"),
|
SamplingMessage::assistant_text("Hello!"),
|
||||||
"test-model".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);
|
||||||
|
|
||||||
assert!(result.validate().is_ok());
|
assert!(result.validate().is_ok());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ use rmcp::{
|
||||||
model::{CallToolRequestParams, ClientInfo, ErrorCode, JsonObject},
|
model::{CallToolRequestParams, ClientInfo, ErrorCode, JsonObject},
|
||||||
tool, tool_handler, tool_router,
|
tool, tool_handler, tool_router,
|
||||||
};
|
};
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
/// Server with tools having different task support modes.
|
/// Server with tools having different task support modes.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -75,8 +74,8 @@ impl ClientHandler for DummyClientHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper to create a task object for tool calls
|
/// Helper to create a task object for tool calls
|
||||||
fn make_task() -> Option<JsonObject> {
|
fn make_task() -> JsonObject {
|
||||||
Some(json!({}).as_object().unwrap().clone())
|
serde_json::Map::new()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ impl ClientHandler for SamplingDemoClient {
|
||||||
SamplingMessage::assistant_text(response_text),
|
SamplingMessage::assistant_text(response_text),
|
||||||
"mock_llm".to_string(),
|
"mock_llm".to_string(),
|
||||||
)
|
)
|
||||||
.with_stop_reason(Some(CreateMessageResult::STOP_REASON_END_TURN.to_string())))
|
.with_stop_reason(CreateMessageResult::STOP_REASON_END_TURN))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue