From b36ce69049fc9fc510fe8e835cdcb402853e938f Mon Sep 17 00:00:00 2001 From: Dale Seo <5466341+DaleSeo@users.noreply.github.com> Date: Wed, 24 Jun 2026 14:48:47 -0400 Subject: [PATCH 1/2] feat!: align model types with MCP 2025-11-25 spec --- conformance/src/bin/client.rs | 59 +- conformance/src/bin/server.rs | 137 +- crates/rmcp-macros/src/task_handler.rs | 10 +- crates/rmcp/src/handler/client.rs | 47 +- crates/rmcp/src/handler/server.rs | 34 +- crates/rmcp/src/handler/server/prompt.rs | 1 + crates/rmcp/src/handler/server/router/tool.rs | 8 +- crates/rmcp/src/handler/server/tool.rs | 2 +- crates/rmcp/src/model.rs | 430 +++-- crates/rmcp/src/model/annotated.rs | 201 +-- crates/rmcp/src/model/capabilities.rs | 59 +- crates/rmcp/src/model/content.rs | 381 ++-- crates/rmcp/src/model/elicitation_schema.rs | 196 ++- crates/rmcp/src/model/meta.rs | 18 +- crates/rmcp/src/model/prompt.rs | 238 +-- crates/rmcp/src/model/resource.rs | 313 ++-- crates/rmcp/src/model/task.rs | 63 +- crates/rmcp/src/service.rs | 26 +- crates/rmcp/src/service/server.rs | 14 +- .../streamable_http_server/session/local.rs | 25 +- crates/rmcp/tests/common/handlers.rs | 10 +- crates/rmcp/tests/test_completion.rs | 29 +- crates/rmcp/tests/test_complex_schema.rs | 2 +- crates/rmcp/tests/test_deserialization.rs | 3 +- crates/rmcp/tests/test_elicitation.rs | 448 +++-- .../rmcp/tests/test_embedded_resource_meta.rs | 46 +- .../tests/test_inflight_response_drain.rs | 2 +- crates/rmcp/tests/test_logging.rs | 29 +- .../client_json_rpc_message_schema.json | 908 ++++++---- ...lient_json_rpc_message_schema_current.json | 908 ++++++---- .../server_json_rpc_message_schema.json | 1528 ++++++++--------- ...erver_json_rpc_message_schema_current.json | 1528 ++++++++--------- crates/rmcp/tests/test_notification.rs | 2 +- crates/rmcp/tests/test_progress_subscriber.rs | 11 +- .../tests/test_prompt_macro_annotations.rs | 37 +- crates/rmcp/tests/test_prompt_macros.rs | 35 +- crates/rmcp/tests/test_prompt_routers.rs | 10 +- .../tests/test_request_timeout_progress.rs | 25 +- crates/rmcp/tests/test_resource_link.rs | 26 +- .../tests/test_resource_link_integration.rs | 53 +- crates/rmcp/tests/test_sampling.rs | 49 +- .../rmcp/tests/test_sse_concurrent_streams.rs | 6 +- crates/rmcp/tests/test_structured_output.rs | 5 +- .../tests/test_task_support_validation.rs | 10 +- crates/rmcp/tests/test_tool_macros.rs | 8 +- crates/rmcp/tests/test_tool_result_meta.rs | 6 +- examples/clients/src/task_stdio.rs | 20 +- examples/servers/src/common/counter.rs | 36 +- examples/servers/src/common/progress_demo.rs | 14 +- examples/servers/src/common/task_demo.rs | 6 +- examples/servers/src/completion_stdio.rs | 19 +- .../servers/src/elicitation_enum_inference.rs | 6 +- examples/servers/src/elicitation_stdio.rs | 19 +- examples/servers/src/prompt_stdio.rs | 43 +- examples/servers/src/sampling_stdio.rs | 2 +- 55 files changed, 4162 insertions(+), 3989 deletions(-) diff --git a/conformance/src/bin/client.rs b/conformance/src/bin/client.rs index 41a94f701..fc3d9f3bb 100644 --- a/conformance/src/bin/client.rs +++ b/conformance/src/bin/client.rs @@ -45,48 +45,46 @@ struct ElicitationDefaultsClientHandler; impl ClientHandler for ElicitationDefaultsClientHandler { fn get_info(&self) -> ClientInfo { let mut info = ClientInfo::default(); - info.capabilities.elicitation = Some(ElicitationCapability { - form: Some(FormElicitationCapability { - schema_validation: Some(true), - }), - url: None, - }); + info.capabilities.elicitation = Some( + ElicitationCapability::new() + .with_form(FormElicitationCapability::new().with_schema_validation(true)), + ); info } async fn create_elicitation( &self, - request: CreateElicitationRequestParams, + request: ElicitRequestParams, _cx: RequestContext, - ) -> Result { + ) -> Result { let content = match &request { - CreateElicitationRequestParams::FormElicitationParams { + ElicitRequestParams::FormElicitationParams { requested_schema, .. } => { let mut defaults = serde_json::Map::new(); for (name, prop) in &requested_schema.properties { match prop { - PrimitiveSchema::String(s) => { + PrimitiveSchemaDefinition::String(s) => { if let Some(d) = &s.default { defaults.insert(name.clone(), Value::String(d.clone())); } } - PrimitiveSchema::Number(n) => { + PrimitiveSchemaDefinition::Number(n) => { if let Some(d) = n.default { defaults.insert(name.clone(), json!(d)); } } - PrimitiveSchema::Integer(i) => { + PrimitiveSchemaDefinition::Integer(i) => { if let Some(d) = i.default { defaults.insert(name.clone(), json!(d)); } } - PrimitiveSchema::Boolean(b) => { + PrimitiveSchemaDefinition::Boolean(b) => { if let Some(d) = b.default { defaults.insert(name.clone(), Value::Bool(d)); } } - PrimitiveSchema::Enum(e) => { + PrimitiveSchemaDefinition::Enum(e) => { let val = match e { EnumSchema::Single(SingleSelectEnumSchema::Untitled(u)) => { u.default.as_ref().map(|d| Value::String(d.clone())) @@ -109,22 +107,22 @@ impl ClientHandler for ElicitationDefaultsClientHandler { }) } EnumSchema::Legacy(_) => None, + _ => None, }; if let Some(v) = val { defaults.insert(name.clone(), v); } } + _ => {} } } Some(Value::Object(defaults)) } _ => Some(json!({})), }; - Ok(CreateElicitationResult { - action: ElicitationAction::Accept, - content, - meta: None, - }) + let mut result = ElicitResult::new(ElicitationAction::Accept); + result.content = content; + Ok(result) } } @@ -134,12 +132,10 @@ struct FullClientHandler; impl ClientHandler for FullClientHandler { fn get_info(&self) -> ClientInfo { let mut info = ClientInfo::default(); - info.capabilities.elicitation = Some(ElicitationCapability { - form: Some(FormElicitationCapability { - schema_validation: Some(true), - }), - url: None, - }); + info.capabilities.elicitation = Some( + ElicitationCapability::new() + .with_form(FormElicitationCapability::new().with_schema_validation(true)), + ); info } @@ -158,7 +154,7 @@ impl ClientHandler for FullClientHandler { Ok(CreateMessageResult::new( SamplingMessage::new( Role::Assistant, - SamplingMessageContent::text(format!( + SamplingMessageContentBlock::text(format!( "This is a mock LLM response to: {}", prompt_text )), @@ -170,14 +166,11 @@ impl ClientHandler for FullClientHandler { async fn create_elicitation( &self, - _request: CreateElicitationRequestParams, + _request: ElicitRequestParams, _cx: RequestContext, - ) -> Result { - Ok(CreateElicitationResult { - action: ElicitationAction::Accept, - content: Some(json!({"username": "testuser", "email": "test@example.com"})), - meta: None, - }) + ) -> Result { + Ok(ElicitResult::new(ElicitationAction::Accept) + .with_content(json!({"username": "testuser", "email": "test@example.com"}))) } } diff --git a/conformance/src/bin/server.rs b/conformance/src/bin/server.rs index c3424f612..b0b0d635c 100644 --- a/conformance/src/bin/server.rs +++ b/conformance/src/bin/server.rs @@ -217,25 +217,21 @@ impl ServerHandler for ConformanceServer { ) -> Result { let args = request.arguments.unwrap_or_default(); match request.name.as_ref() { - "test_simple_text" => Ok(CallToolResult::success(vec![Content::text( + "test_simple_text" => Ok(CallToolResult::success(vec![ContentBlock::text( "This is a simple text response for testing.", )])), - "test_image_content" => Ok(CallToolResult::success(vec![Content::image( + "test_image_content" => Ok(CallToolResult::success(vec![ContentBlock::image( TEST_IMAGE_DATA, "image/png", )])), "test_audio_content" => { - let audio = RawContent::Audio(RawAudioContent { - data: TEST_AUDIO_DATA.into(), - mime_type: "audio/wav".into(), - }) - .no_annotation(); + let audio = ContentBlock::Audio(AudioContent::new(TEST_AUDIO_DATA, "audio/wav")); Ok(CallToolResult::success(vec![audio])) } - "test_embedded_resource" => Ok(CallToolResult::success(vec![Content::resource( + "test_embedded_resource" => Ok(CallToolResult::success(vec![ContentBlock::resource( ResourceContents::TextResourceContents { uri: "test://embedded-resource".into(), mime_type: Some("text/plain".into()), @@ -245,9 +241,9 @@ impl ServerHandler for ConformanceServer { )])), "test_multiple_content_types" => Ok(CallToolResult::success(vec![ - Content::text("Multiple content types test:"), - Content::image(TEST_IMAGE_DATA, "image/png"), - Content::resource(ResourceContents::TextResourceContents { + ContentBlock::text("Multiple content types test:"), + ContentBlock::image(TEST_IMAGE_DATA, "image/png"), + ContentBlock::resource(ResourceContents::TextResourceContents { uri: "test://mixed-content-resource".into(), mime_type: Some("application/json".into()), text: r#"{"test":"data","value":123}"#.into(), @@ -263,21 +259,20 @@ impl ServerHandler for ConformanceServer { ] { let _ = cx .peer - .notify_logging_message(LoggingMessageNotificationParam { - level: LoggingLevel::Info, - logger: Some("conformance-server".into()), - data: json!(msg), - }) + .notify_logging_message( + LoggingMessageNotificationParam::new(LoggingLevel::Info, json!(msg)) + .with_logger("conformance-server"), + ) .await; tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; } - Ok(CallToolResult::success(vec![Content::text( + Ok(CallToolResult::success(vec![ContentBlock::text( "Logging test completed", )])) } - "test_error_handling" => Ok(CallToolResult::error(vec![Content::text( + "test_error_handling" => Ok(CallToolResult::error(vec![ContentBlock::text( "This tool intentionally returns an error for testing", )])), @@ -290,18 +285,17 @@ impl ServerHandler for ConformanceServer { if let Some(token) = &progress_token { let _ = cx .peer - .notify_progress(ProgressNotificationParam { - progress_token: token.clone(), - progress, - total: Some(100.0), - message: Some(message.into()), - }) + .notify_progress( + ProgressNotificationParam::new(token.clone(), progress) + .with_total(100.0) + .with_message(message), + ) .await; } tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; } - Ok(CallToolResult::success(vec![Content::text( + Ok(CallToolResult::success(vec![ContentBlock::text( "Progress test completed", )])) } @@ -328,12 +322,12 @@ impl ServerHandler for ConformanceServer { .and_then(|c| c.as_text()) .map(|t| t.text.clone()) .unwrap_or_else(|| "No text response".into()); - Ok(CallToolResult::success(vec![Content::text(format!( + Ok(CallToolResult::success(vec![ContentBlock::text(format!( "LLM response: {}", text ))])) } - Err(e) => Ok(CallToolResult::error(vec![Content::text(format!( + Err(e) => Ok(CallToolResult::error(vec![ContentBlock::text(format!( "Sampling error: {}", e ))])), @@ -365,23 +359,24 @@ impl ServerHandler for ConformanceServer { match cx .peer - .create_elicitation(CreateElicitationRequestParams::FormElicitationParams { + .create_elicitation(ElicitRequestParams::FormElicitationParams { meta: None, message: message.into(), requested_schema: schema, }) .await { - Ok(result) => Ok(CallToolResult::success(vec![Content::text(format!( + Ok(result) => Ok(CallToolResult::success(vec![ContentBlock::text(format!( "User response: action={}, content={:?}", match result.action { ElicitationAction::Accept => "accept", ElicitationAction::Decline => "decline", ElicitationAction::Cancel => "cancel", + _ => "unknown", }, result.content ))])), - Err(e) => Ok(CallToolResult::error(vec![Content::text(format!( + Err(e) => Ok(CallToolResult::error(vec![ContentBlock::text(format!( "Elicitation error: {}", e ))])), @@ -425,23 +420,24 @@ impl ServerHandler for ConformanceServer { match cx .peer - .create_elicitation(CreateElicitationRequestParams::FormElicitationParams { + .create_elicitation(ElicitRequestParams::FormElicitationParams { meta: None, message: "Please provide values (all have defaults)".into(), requested_schema: schema, }) .await { - Ok(result) => Ok(CallToolResult::success(vec![Content::text(format!( + Ok(result) => Ok(CallToolResult::success(vec![ContentBlock::text(format!( "Elicitation completed: action={}, content={:?}", match result.action { ElicitationAction::Accept => "accept", ElicitationAction::Decline => "decline", ElicitationAction::Cancel => "cancel", + _ => "unknown", }, result.content ))])), - Err(e) => Ok(CallToolResult::error(vec![Content::text(format!( + Err(e) => Ok(CallToolResult::error(vec![ContentBlock::text(format!( "Elicitation error: {}", e ))])), @@ -493,22 +489,23 @@ impl ServerHandler for ConformanceServer { match cx .peer - .create_elicitation(CreateElicitationRequestParams::FormElicitationParams { + .create_elicitation(ElicitRequestParams::FormElicitationParams { meta: None, message: "Test enum schema improvements".into(), requested_schema: schema, }) .await { - Ok(result) => Ok(CallToolResult::success(vec![Content::text(format!( + Ok(result) => Ok(CallToolResult::success(vec![ContentBlock::text(format!( "Enum elicitation completed: action={}", match result.action { ElicitationAction::Accept => "accept", ElicitationAction::Decline => "decline", ElicitationAction::Cancel => "cancel", + _ => "unknown", } ))])), - Err(e) => Ok(CallToolResult::error(vec![Content::text(format!( + Err(e) => Ok(CallToolResult::error(vec![ContentBlock::text(format!( "Elicitation error: {}", e ))])), @@ -517,7 +514,7 @@ impl ServerHandler for ConformanceServer { "json_schema_2020_12_tool" => { let name = args.get("name").and_then(|v| v.as_str()).unwrap_or("world"); - Ok(CallToolResult::success(vec![Content::text(format!( + Ok(CallToolResult::success(vec![ContentBlock::text(format!( "Hello, {}!", name ))])) @@ -525,7 +522,7 @@ impl ServerHandler for ConformanceServer { "test_reconnection" => { tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; - Ok(CallToolResult::success(vec![Content::text( + Ok(CallToolResult::success(vec![ContentBlock::text( "Reconnection test completed", )])) } @@ -545,28 +542,12 @@ impl ServerHandler for ConformanceServer { Ok(ListResourcesResult { meta: None, resources: vec![ - RawResource { - uri: "test://static-text".into(), - name: "Static Text Resource".into(), - title: None, - description: Some("A static text resource for testing".into()), - mime_type: Some("text/plain".into()), - size: None, - icons: None, - meta: None, - } - .no_annotation(), - RawResource { - uri: "test://static-binary".into(), - name: "Static Binary Resource".into(), - title: None, - description: Some("A static binary/blob resource for testing".into()), - mime_type: Some("image/png".into()), - size: None, - icons: None, - meta: None, - } - .no_annotation(), + Resource::new("test://static-text", "Static Text Resource") + .with_description("A static text resource for testing") + .with_mime_type("text/plain"), + Resource::new("test://static-binary", "Static Binary Resource") + .with_description("A static binary/blob resource for testing") + .with_mime_type("image/png"), ], next_cursor: None, }) @@ -630,15 +611,9 @@ impl ServerHandler for ConformanceServer { Ok(ListResourceTemplatesResult { meta: None, resource_templates: vec![ - RawResourceTemplate { - uri_template: "test://template/{id}/data".into(), - name: "Dynamic Resource".into(), - title: None, - description: Some("A dynamic resource with parameter substitution".into()), - mime_type: Some("application/json".into()), - icons: None, - } - .no_annotation(), + ResourceTemplate::new("test://template/{id}/data", "Dynamic Resource") + .with_description("A dynamic resource with parameter substitution") + .with_mime_type("application/json"), ], next_cursor: None, }) @@ -711,7 +686,7 @@ impl ServerHandler for ConformanceServer { ) -> Result { match request.name.as_str() { "test_simple_prompt" => Ok(GetPromptResult::new(vec![PromptMessage::new_text( - PromptMessageRole::User, + Role::User, "This is a simple test prompt.", )]) .with_description("A simple test prompt")), @@ -723,15 +698,15 @@ impl ServerHandler for ConformanceServer { .and_then(|v| v.as_str()) .unwrap_or("friendly"); Ok(GetPromptResult::new(vec![PromptMessage::new_text( - PromptMessageRole::User, + Role::User, format!("Please greet {} in a {} style.", name, style), )]) .with_description("A prompt with arguments")) } "test_prompt_with_embedded_resource" => Ok(GetPromptResult::new(vec![ - PromptMessage::new_text(PromptMessageRole::User, "Here is a resource:"), + PromptMessage::new_text(Role::User, "Here is a resource:"), PromptMessage::new_resource( - PromptMessageRole::User, + Role::User, "test://static-text".into(), Some("text/plain".into()), Some("Resource content for prompt".into()), @@ -742,19 +717,10 @@ impl ServerHandler for ConformanceServer { ]) .with_description("A prompt with an embedded resource")), "test_prompt_with_image" => { - let image_content = RawImageContent { - data: TEST_IMAGE_DATA.into(), - mime_type: "image/png".into(), - meta: None, - }; + let image_content = ImageContent::new(TEST_IMAGE_DATA, "image/png"); Ok(GetPromptResult::new(vec![ - PromptMessage::new_text(PromptMessageRole::User, "Here is an image:"), - PromptMessage::new( - PromptMessageRole::User, - PromptMessageContent::Image { - image: image_content.no_annotation(), - }, - ), + PromptMessage::new_text(Role::User, "Here is an image:"), + PromptMessage::new(Role::User, ContentBlock::Image(image_content)), ]) .with_description("A prompt with an image")) } @@ -787,6 +753,7 @@ impl ServerHandler for ConformanceServer { vec![prompt_ref.name.clone()] } } + _ => vec![], }; Ok(CompleteResult::new( CompletionInfo::new(values).map_err(|e| ErrorData::internal_error(e, None))?, diff --git a/crates/rmcp-macros/src/task_handler.rs b/crates/rmcp-macros/src/task_handler.rs index ba8df4b96..5815fd35f 100644 --- a/crates/rmcp-macros/src/task_handler.rs +++ b/crates/rmcp-macros/src/task_handler.rs @@ -111,7 +111,7 @@ pub fn task_handler(attr: TokenStream, input: TokenStream) -> syn::Result, ) -> Result { use rmcp::task_manager::current_timestamp; @@ -145,7 +145,7 @@ pub fn task_handler(attr: TokenStream, input: TokenStream) -> syn::Result syn::Result syn::Result, ) -> Result { use std::time::Duration; @@ -242,7 +242,7 @@ pub fn task_handler(attr: TokenStream, input: TokenStream) -> syn::Result Service for H { .list_roots(context) .await .map(ClientResult::ListRootsResult), - ServerRequest::CreateElicitationRequest(request) => self + ServerRequest::ElicitRequest(request) => self .create_elicitation(request.params, context) .await - .map(ClientResult::CreateElicitationResult), + .map(ClientResult::ElicitResult), ServerRequest::CustomRequest(request) => self .on_custom_request(request, context) .await @@ -64,10 +64,13 @@ impl Service for H { ServerNotification::PromptListChangedNotification(_notification_no_param) => { self.on_prompt_list_changed(context).await } - ServerNotification::ElicitationCompletionNotification(notification) => { + ServerNotification::ElicitationCompleteNotification(notification) => { self.on_url_elicitation_notification_complete(notification.params, context) .await } + ServerNotification::TaskStatusNotification(notification) => { + self.on_task_status(notification.params, context).await + } ServerNotification::CustomNotification(notification) => { self.on_custom_notification(notification, context).await } @@ -138,12 +141,12 @@ pub trait ClientHandler: Sized + Send + Sync + 'static { /// &self, /// request: CreateElicitationRequestParam, /// context: RequestContext, - /// ) -> Result { + /// ) -> Result { /// match request { /// CreateElicitationRequestParam::FormElicitationParam {meta, message, requested_schema,} => { /// // Display message to user and collect input according to requested_schema /// let user_input = get_user_input(message, requested_schema).await?; - /// Ok(CreateElicitationResult { + /// Ok(ElicitResult { /// action: ElicitationAction::Accept, /// content: Some(user_input), /// meta: None, @@ -152,7 +155,7 @@ pub trait ClientHandler: Sized + Send + Sync + 'static { /// CreateElicitationRequestParam::UrlElicitationParam {meta, message, url, elicitation_id,} => { /// // Open URL in browser for user to complete elicitation /// open_url_in_browser(url).await?; - /// Ok(CreateElicitationResult { + /// Ok(ElicitResult { /// action: ElicitationAction::Accept, /// content: None, /// meta: None, @@ -164,13 +167,12 @@ pub trait ClientHandler: Sized + Send + Sync + 'static { /// ``` fn create_elicitation( &self, - request: CreateElicitationRequestParams, + request: ElicitRequestParams, context: RequestContext, - ) -> impl Future> + MaybeSendFuture + '_ - { + ) -> impl Future> + MaybeSendFuture + '_ { // Default implementation declines all requests - real clients should override this let _ = (request, context); - std::future::ready(Ok(CreateElicitationResult { + std::future::ready(Ok(ElicitResult { action: ElicitationAction::Decline, content: None, meta: None, @@ -245,6 +247,13 @@ pub trait ClientHandler: Sized + Send + Sync + 'static { ) -> impl Future + MaybeSendFuture + '_ { std::future::ready(()) } + fn on_task_status( + &self, + params: TaskStatusNotificationParam, + context: NotificationContext, + ) -> impl Future + MaybeSendFuture + '_ { + std::future::ready(()) + } fn on_custom_notification( &self, notification: CustomNotification, @@ -283,22 +292,24 @@ macro_rules! impl_client_handler_for_wrapper { &self, params: CreateMessageRequestParams, context: RequestContext, - ) -> impl Future> + MaybeSendFuture + '_ { + ) -> impl Future> + MaybeSendFuture + '_ + { (**self).create_message(params, context) } fn list_roots( &self, context: RequestContext, - ) -> impl Future> + MaybeSendFuture + '_ { + ) -> impl Future> + MaybeSendFuture + '_ + { (**self).list_roots(context) } fn create_elicitation( &self, - request: CreateElicitationRequestParams, + request: ElicitRequestParams, context: RequestContext, - ) -> impl Future> + MaybeSendFuture + '_ { + ) -> impl Future> + MaybeSendFuture + '_ { (**self).create_elicitation(request, context) } @@ -363,6 +374,14 @@ macro_rules! impl_client_handler_for_wrapper { (**self).on_prompt_list_changed(context) } + fn on_task_status( + &self, + params: TaskStatusNotificationParam, + context: NotificationContext, + ) -> impl Future + MaybeSendFuture + '_ { + (**self).on_task_status(params, context) + } + fn on_custom_notification( &self, notification: CustomNotification, diff --git a/crates/rmcp/src/handler/server.rs b/crates/rmcp/src/handler/server.rs index aea596703..54964559d 100644 --- a/crates/rmcp/src/handler/server.rs +++ b/crates/rmcp/src/handler/server.rs @@ -117,11 +117,11 @@ impl Service for H { .list_tasks(request.params, context) .await .map(ServerResult::ListTasksResult), - ClientRequest::GetTaskInfoRequest(request) => self + ClientRequest::GetTaskRequest(request) => self .get_task_info(request.params, context) .await .map(ServerResult::GetTaskResult), - ClientRequest::GetTaskResultRequest(request) => self + ClientRequest::GetTaskPayloadRequest(request) => self .get_task_result(request.params, context) .await .map(ServerResult::GetTaskPayloadResult), @@ -161,6 +161,9 @@ impl Service for H { ClientNotification::RootsListChangedNotification(_notification) => { self.on_roots_list_changed(context).await } + ClientNotification::TaskStatusNotification(notification) => { + self.on_task_status(notification.params, context).await + } ClientNotification::CustomNotification(notification) => { self.on_custom_notification(notification, context).await } @@ -359,6 +362,13 @@ macro_rules! server_handler_methods { ) -> impl Future + MaybeSendFuture + '_ { std::future::ready(()) } + fn on_task_status( + &self, + params: TaskStatusNotificationParam, + context: NotificationContext, + ) -> impl Future + MaybeSendFuture + '_ { + std::future::ready(()) + } fn on_custom_notification( &self, notification: CustomNotification, @@ -382,20 +392,20 @@ macro_rules! server_handler_methods { fn get_task_info( &self, - request: GetTaskInfoParams, + request: GetTaskParams, context: RequestContext, ) -> impl Future> + MaybeSendFuture + '_ { let _ = (request, context); - std::future::ready(Err(McpError::method_not_found::())) + std::future::ready(Err(McpError::method_not_found::())) } fn get_task_result( &self, - request: GetTaskResultParams, + request: GetTaskPayloadParams, context: RequestContext, ) -> impl Future> + MaybeSendFuture + '_ { let _ = (request, context); - std::future::ready(Err(McpError::method_not_found::())) + std::future::ready(Err(McpError::method_not_found::())) } fn cancel_task( @@ -578,6 +588,14 @@ macro_rules! impl_server_handler_for_wrapper { (**self).on_roots_list_changed(context) } + fn on_task_status( + &self, + params: TaskStatusNotificationParam, + context: NotificationContext, + ) -> impl Future + MaybeSendFuture + '_ { + (**self).on_task_status(params, context) + } + fn on_custom_notification( &self, notification: CustomNotification, @@ -600,7 +618,7 @@ macro_rules! impl_server_handler_for_wrapper { fn get_task_info( &self, - request: GetTaskInfoParams, + request: GetTaskParams, context: RequestContext, ) -> impl Future> + MaybeSendFuture + '_ { (**self).get_task_info(request, context) @@ -608,7 +626,7 @@ macro_rules! impl_server_handler_for_wrapper { fn get_task_result( &self, - request: GetTaskResultParams, + request: GetTaskPayloadParams, context: RequestContext, ) -> impl Future> + MaybeSendFuture + '_ { (**self).get_task_result(request, context) diff --git a/crates/rmcp/src/handler/server/prompt.rs b/crates/rmcp/src/handler/server/prompt.rs index 11ca4bf83..c291ef70b 100644 --- a/crates/rmcp/src/handler/server/prompt.rs +++ b/crates/rmcp/src/handler/server/prompt.rs @@ -105,6 +105,7 @@ impl IntoGetPromptResult for Vec { Ok(GetPromptResult { description: None, messages: self, + meta: None, }) } } diff --git a/crates/rmcp/src/handler/server/router/tool.rs b/crates/rmcp/src/handler/server/router/tool.rs index 35bd25a97..ae096c00c 100644 --- a/crates/rmcp/src/handler/server/router/tool.rs +++ b/crates/rmcp/src/handler/server/router/tool.rs @@ -137,7 +137,7 @@ use crate::{ tool::{CallToolHandler, DynCallToolHandler, ToolCallContext}, tool_name_validation::validate_and_warn_tool_name, }, - model::{CallToolResult, Content, ErrorCode, Tool, ToolAnnotations}, + model::{CallToolResult, ContentBlock, ErrorCode, Tool, ToolAnnotations}, service::{MaybeBoxFuture, MaybeSend}, }; @@ -149,7 +149,9 @@ fn into_tool_argument_error(error: crate::ErrorData) -> Result { pub service: &'s S, pub name: Cow<'static, str>, pub arguments: Option, - pub task: Option, + pub task: Option, } impl<'s, S> ToolCallContext<'s, S> { diff --git a/crates/rmcp/src/model.rs b/crates/rmcp/src/model.rs index 5fb7eb9ed..7f3ff38e2 100644 --- a/crates/rmcp/src/model.rs +++ b/crates/rmcp/src/model.rs @@ -698,10 +698,23 @@ impl CustomResult { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] +#[non_exhaustive] pub struct CancelledNotificationParam { - pub request_id: RequestId, + #[serde(skip_serializing_if = "Option::is_none")] + pub request_id: Option, pub reason: Option, + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option, +} + +impl CancelledNotificationParam { + pub fn new(request_id: Option, reason: Option) -> Self { + Self { + request_id, + reason, + meta: None, + } + } } const_string!(CancelledNotificationMethod = "notifications/cancelled"); @@ -864,6 +877,8 @@ pub struct InitializeResult { /// Optional human-readable instructions about using this server #[serde(skip_serializing_if = "Option::is_none")] pub instructions: Option, + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option, } impl InitializeResult { @@ -874,6 +889,7 @@ impl InitializeResult { capabilities, server_info: Implementation::from_build_env(), instructions: None, + meta: None, } } @@ -907,6 +923,7 @@ impl Default for ServerInfo { capabilities: ServerCapabilities::default(), server_info: Implementation::from_build_env(), instructions: None, + meta: None, } } } @@ -1107,7 +1124,7 @@ const_string!(ProgressNotificationMethod = "notifications/progress"); #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] +#[non_exhaustive] pub struct ProgressNotificationParam { pub progress_token: ProgressToken, /// The progress thus far. This should increase every time progress is made, even if the total is unknown. @@ -1118,6 +1135,8 @@ pub struct ProgressNotificationParam { /// An optional message describing the current progress. #[serde(skip_serializing_if = "Option::is_none")] pub message: Option, + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option, } impl ProgressNotificationParam { @@ -1128,6 +1147,7 @@ impl ProgressNotificationParam { progress, total: None, message: None, + meta: None, } } @@ -1250,12 +1270,17 @@ pub type ReadResourceRequestParam = ReadResourceRequestParams; pub struct ReadResourceResult { /// The actual content of the resource pub contents: Vec, + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option, } impl ReadResourceResult { /// Create a new ReadResourceResult with the given contents. pub fn new(contents: Vec) -> Self { - Self { contents } + Self { + contents, + meta: None, + } } } @@ -1352,16 +1377,21 @@ const_string!(ResourceUpdatedNotificationMethod = "notifications/resources/updat #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] +#[non_exhaustive] pub struct ResourceUpdatedNotificationParam { /// The URI of the resource that was updated pub uri: String, + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option, } impl ResourceUpdatedNotificationParam { /// Create a new ResourceUpdatedNotificationParam. pub fn new(uri: impl Into) -> Self { - Self { uri: uri.into() } + Self { + uri: uri.into(), + meta: None, + } } } @@ -1506,7 +1536,7 @@ const_string!(LoggingMessageNotificationMethod = "notifications/message"); #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] +#[non_exhaustive] pub struct LoggingMessageNotificationParam { /// The severity level of this log message pub level: LoggingLevel, @@ -1515,6 +1545,8 @@ pub struct LoggingMessageNotificationParam { pub logger: Option, /// The actual log data pub data: Value, + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option, } impl LoggingMessageNotificationParam { @@ -1524,6 +1556,7 @@ impl LoggingMessageNotificationParam { level, logger: None, data, + meta: None, } } @@ -1564,7 +1597,7 @@ pub enum Role { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] #[serde(rename_all = "lowercase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")] +#[non_exhaustive] pub enum ToolChoiceMode { /// Model decides whether to use tools #[default] @@ -1666,11 +1699,11 @@ impl SamplingContent { } } -impl SamplingMessageContent { +impl SamplingMessageContentBlock { /// Get the text content if this is a Text variant - pub fn as_text(&self) -> Option<&RawTextContent> { + pub fn as_text(&self) -> Option<&TextContent> { match self { - SamplingMessageContent::Text(text) => Some(text), + SamplingMessageContentBlock::Text(text) => Some(text), _ => None, } } @@ -1678,7 +1711,7 @@ impl SamplingMessageContent { /// Get the tool use content if this is a ToolUse variant pub fn as_tool_use(&self) -> Option<&ToolUseContent> { match self { - SamplingMessageContent::ToolUse(tool_use) => Some(tool_use), + SamplingMessageContentBlock::ToolUse(tool_use) => Some(tool_use), _ => None, } } @@ -1686,7 +1719,7 @@ impl SamplingMessageContent { /// Get the tool result content if this is a ToolResult variant pub fn as_tool_result(&self) -> Option<&ToolResultContent> { match self { - SamplingMessageContent::ToolResult(tool_result) => Some(tool_result), + SamplingMessageContentBlock::ToolResult(tool_result) => Some(tool_result), _ => None, } } @@ -1716,7 +1749,7 @@ pub struct SamplingMessage { /// The role of the message sender (User or Assistant) pub role: Role, /// The actual content of the message (text, image, audio, tool use, or tool result) - pub content: SamplingContent, + pub content: SamplingContent, #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] pub meta: Option, } @@ -1725,37 +1758,37 @@ pub struct SamplingMessage { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")] -pub enum SamplingMessageContent { - Text(RawTextContent), - Image(RawImageContent), - Audio(RawAudioContent), +#[non_exhaustive] +pub enum SamplingMessageContentBlock { + Text(TextContent), + Image(ImageContent), + Audio(AudioContent), /// Assistant only ToolUse(ToolUseContent), /// User only ToolResult(ToolResultContent), } -impl SamplingMessageContent { +#[deprecated(since = "2.0.0", note = "Renamed to SamplingMessageContentBlock")] +pub type SamplingMessageContent = SamplingMessageContentBlock; + +impl SamplingMessageContentBlock { /// Create a text content pub fn text(text: impl Into) -> Self { - Self::Text(RawTextContent { - text: text.into(), - meta: None, - }) + Self::Text(TextContent::new(text)) } pub fn tool_use(id: impl Into, name: impl Into, input: JsonObject) -> Self { Self::ToolUse(ToolUseContent::new(id, name, input)) } - pub fn tool_result(tool_use_id: impl Into, content: Vec) -> Self { + pub fn tool_result(tool_use_id: impl Into, content: Vec) -> Self { Self::ToolResult(ToolResultContent::new(tool_use_id, content)) } } impl SamplingMessage { - pub fn new(role: Role, content: impl Into) -> Self { + pub fn new(role: Role, content: impl Into) -> Self { Self { role, content: SamplingContent::Single(content.into()), @@ -1763,7 +1796,7 @@ impl SamplingMessage { } } - pub fn new_multiple(role: Role, contents: Vec) -> Self { + pub fn new_multiple(role: Role, contents: Vec) -> Self { Self { role, content: SamplingContent::Multiple(contents), @@ -1772,17 +1805,17 @@ impl SamplingMessage { } pub fn user_text(text: impl Into) -> Self { - Self::new(Role::User, SamplingMessageContent::text(text)) + Self::new(Role::User, SamplingMessageContentBlock::text(text)) } pub fn assistant_text(text: impl Into) -> Self { - Self::new(Role::Assistant, SamplingMessageContent::text(text)) + Self::new(Role::Assistant, SamplingMessageContentBlock::text(text)) } - pub fn user_tool_result(tool_use_id: impl Into, content: Vec) -> Self { + pub fn user_tool_result(tool_use_id: impl Into, content: Vec) -> Self { Self::new( Role::User, - SamplingMessageContent::tool_result(tool_use_id, content), + SamplingMessageContentBlock::tool_result(tool_use_id, content), ) } @@ -1793,56 +1826,52 @@ impl SamplingMessage { ) -> Self { Self::new( Role::Assistant, - SamplingMessageContent::tool_use(id, name, input), + SamplingMessageContentBlock::tool_use(id, name, input), ) } } -// Conversion from RawTextContent to SamplingMessageContent -impl From for SamplingMessageContent { - fn from(text: RawTextContent) -> Self { - SamplingMessageContent::Text(text) +impl From for SamplingMessageContentBlock { + fn from(text: TextContent) -> Self { + SamplingMessageContentBlock::Text(text) } } -// Conversion from String to SamplingMessageContent (as text) -impl From for SamplingMessageContent { +// Conversion from String to SamplingMessageContentBlock (as text) +impl From for SamplingMessageContentBlock { fn from(text: String) -> Self { - SamplingMessageContent::text(text) + SamplingMessageContentBlock::text(text) } } -impl From<&str> for SamplingMessageContent { +impl From<&str> for SamplingMessageContentBlock { fn from(text: &str) -> Self { - SamplingMessageContent::text(text) + SamplingMessageContentBlock::text(text) } } -// Backward compatibility: Convert Content to SamplingMessageContent -// Note: Resource and ResourceLink variants are not supported in sampling messages -impl TryFrom for SamplingMessageContent { +impl TryFrom for SamplingMessageContentBlock { type Error = &'static str; - fn try_from(content: Content) -> Result { - match content.raw { - RawContent::Text(text) => Ok(SamplingMessageContent::Text(text)), - RawContent::Image(image) => Ok(SamplingMessageContent::Image(image)), - RawContent::Audio(audio) => Ok(SamplingMessageContent::Audio(audio)), - RawContent::Resource(_) => { + fn try_from(content: ContentBlock) -> Result { + match content { + ContentBlock::Text(text) => Ok(SamplingMessageContentBlock::Text(text)), + ContentBlock::Image(image) => Ok(SamplingMessageContentBlock::Image(image)), + ContentBlock::Audio(audio) => Ok(SamplingMessageContentBlock::Audio(audio)), + ContentBlock::Resource(_) => { Err("Resource content is not supported in sampling messages") } - RawContent::ResourceLink(_) => { + ContentBlock::ResourceLink(_) => { Err("ResourceLink content is not supported in sampling messages") } } } } -// Backward compatibility: Convert Content to SamplingContent -impl TryFrom for SamplingContent { +impl TryFrom for SamplingContent { type Error = &'static str; - fn try_from(content: Content) -> Result { + fn try_from(content: ContentBlock) -> Result { Ok(SamplingContent::Single(content.try_into()?)) } } @@ -1853,7 +1882,7 @@ impl TryFrom for SamplingContent { /// should be provided to the LLM when processing sampling requests. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")] +#[non_exhaustive] pub enum ContextInclusion { /// Include context from all connected MCP servers #[serde(rename = "allServers")] @@ -1884,7 +1913,7 @@ pub struct CreateMessageRequestParams { pub meta: Option, /// Task metadata for async task management (SEP-1319) #[serde(skip_serializing_if = "Option::is_none")] - pub task: Option, + pub task: Option, /// The conversation history and current messages pub messages: Vec, /// Preferences for model selection and behavior @@ -1925,10 +1954,10 @@ impl RequestParamsMeta for CreateMessageRequestParams { } impl TaskAugmentedRequestParamsMeta for CreateMessageRequestParams { - fn task(&self) -> Option<&JsonObject> { + fn task(&self) -> Option<&TaskMetadata> { self.task.as_ref() } - fn task_mut(&mut self) -> &mut Option { + fn task_mut(&mut self) -> &mut Option { &mut self.task } } @@ -2012,10 +2041,10 @@ impl CreateMessageRequestParams { for content in msg.content.iter() { // ToolUse only in assistant messages, ToolResult only in user messages match content { - SamplingMessageContent::ToolUse(_) if msg.role != Role::Assistant => { + SamplingMessageContentBlock::ToolUse(_) if msg.role != Role::Assistant => { return Err("ToolUse content is only allowed in assistant messages".into()); } - SamplingMessageContent::ToolResult(_) if msg.role != Role::User => { + SamplingMessageContentBlock::ToolResult(_) if msg.role != Role::User => { return Err("ToolResult content is only allowed in user messages".into()); } _ => {} @@ -2026,11 +2055,11 @@ impl CreateMessageRequestParams { let contents: Vec<_> = msg.content.iter().collect(); let has_tool_result = contents .iter() - .any(|c| matches!(c, SamplingMessageContent::ToolResult(_))); + .any(|c| matches!(c, SamplingMessageContentBlock::ToolResult(_))); if has_tool_result && contents .iter() - .any(|c| !matches!(c, SamplingMessageContent::ToolResult(_))) + .any(|c| !matches!(c, SamplingMessageContentBlock::ToolResult(_))) { return Err( "SamplingMessage with tool result content MUST NOT contain other content types" @@ -2050,13 +2079,13 @@ impl CreateMessageRequestParams { for msg in &self.messages { if msg.role == Role::Assistant { for content in msg.content.iter() { - if let SamplingMessageContent::ToolUse(tu) = content { + if let SamplingMessageContentBlock::ToolUse(tu) = content { pending_tool_use_ids.push(tu.id.clone()); } } } else if msg.role == Role::User { for content in msg.content.iter() { - if let SamplingMessageContent::ToolResult(tr) = content { + if let SamplingMessageContentBlock::ToolResult(tr) = content { if !pending_tool_use_ids.contains(&tr.tool_use_id) { return Err(format!( "ToolResult with toolUseId '{}' has no matching ToolUse", @@ -2181,7 +2210,7 @@ impl ModelHint { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] +#[non_exhaustive] pub struct CompletionContext { /// Previously resolved argument values that can inform completion suggestions #[serde(skip_serializing_if = "Option::is_none")] @@ -2272,7 +2301,7 @@ pub type CompleteRequest = Request #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] +#[non_exhaustive] pub struct CompletionInfo { pub values: Vec, #[serde(skip_serializing_if = "Option::is_none")] @@ -2354,22 +2383,27 @@ impl CompletionInfo { #[non_exhaustive] pub struct CompleteResult { pub completion: CompletionInfo, + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option, } impl CompleteResult { /// Create a new CompleteResult with the given completion info. pub fn new(completion: CompletionInfo) -> Self { - Self { completion } + Self { + completion, + meta: None, + } } } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(tag = "type")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")] +#[non_exhaustive] pub enum Reference { #[serde(rename = "ref/resource")] - Resource(ResourceReference), + Resource(ResourceTemplateReference), #[serde(rename = "ref/prompt")] Prompt(PromptReference), } @@ -2383,12 +2417,13 @@ impl Reference { Self::Prompt(PromptReference { name: name.into(), title: None, + meta: None, }) } /// Create a resource reference pub fn for_resource(uri: impl Into) -> Self { - Self::Resource(ResourceReference { uri: uri.into() }) + Self::Resource(ResourceTemplateReference { uri: uri.into() }) } /// Get the reference type as a string @@ -2418,11 +2453,20 @@ impl Reference { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] -pub struct ResourceReference { +#[non_exhaustive] +pub struct ResourceTemplateReference { pub uri: String, } +impl ResourceTemplateReference { + pub fn new(uri: impl Into) -> Self { + Self { uri: uri.into() } + } +} + +#[deprecated(since = "2.0.0", note = "Renamed to ResourceTemplateReference")] +pub type ResourceReference = ResourceTemplateReference; + #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[non_exhaustive] @@ -2430,6 +2474,8 @@ pub struct PromptReference { pub name: String, #[serde(skip_serializing_if = "Option::is_none")] pub title: Option, + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option, } impl PromptReference { @@ -2438,6 +2484,7 @@ impl PromptReference { Self { name: name.into(), title: None, + meta: None, } } @@ -2452,12 +2499,21 @@ const_string!(CompleteRequestMethod = "completion/complete"); #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] +#[non_exhaustive] pub struct ArgumentInfo { pub name: String, pub value: String, } +impl ArgumentInfo { + pub fn new(name: impl Into, value: impl Into) -> Self { + Self { + name: name.into(), + value: value.into(), + } + } +} + // ============================================================================= // ROOTS AND WORKSPACE MANAGEMENT // ============================================================================= @@ -2469,6 +2525,8 @@ pub struct Root { pub uri: String, #[serde(skip_serializing_if = "Option::is_none")] pub name: Option, + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option, } impl Root { @@ -2477,6 +2535,7 @@ impl Root { Self { uri: uri.into(), name: None, + meta: None, } } @@ -2485,6 +2544,12 @@ impl Root { self.name = Some(name.into()); self } + + /// Sets the protocol-level metadata for this root. + pub fn with_meta(mut self, meta: Meta) -> Self { + self.meta = Some(meta); + self + } } const_string!(ListRootsRequestMethod = "roots/list"); @@ -2527,7 +2592,7 @@ const_string!(ElicitationCompletionNotificationMethod = "notifications/elicitati #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[serde(rename_all = "lowercase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")] +#[non_exhaustive] pub enum ElicitationAction { /// User accepts the request and provides the requested information Accept, @@ -2567,7 +2632,7 @@ enum CreateElicitationRequestParamDeserializeHelper { }, } -impl TryFrom for CreateElicitationRequestParams { +impl TryFrom for ElicitRequestParams { type Error = serde_json::Error; fn try_from( @@ -2583,7 +2648,7 @@ impl TryFrom for CreateElicitati meta, message, requested_schema, - } => Ok(CreateElicitationRequestParams::FormElicitationParams { + } => Ok(ElicitRequestParams::FormElicitationParams { meta, message, requested_schema, @@ -2593,7 +2658,7 @@ impl TryFrom for CreateElicitati message, url, elicitation_id, - } => Ok(CreateElicitationRequestParams::UrlElicitationParams { + } => Ok(ElicitRequestParams::UrlElicitationParams { meta, message, url, @@ -2614,7 +2679,7 @@ impl TryFrom for CreateElicitati /// ```rust /// use rmcp::model::*; /// -/// let params = CreateElicitationRequestParams::FormElicitationParams { +/// let params = ElicitRequestParams::FormElicitationParams { /// meta: None, /// message: "Please provide your email".to_string(), /// requested_schema: ElicitationSchema::builder() @@ -2626,7 +2691,7 @@ impl TryFrom for CreateElicitati /// 2. URL-based elicitation request /// ```rust /// use rmcp::model::*; -/// let params = CreateElicitationRequestParams::UrlElicitationParams { +/// let params = ElicitRequestParams::UrlElicitationParams { /// meta: None, /// message: "Please provide your feedback at the following URL".to_string(), /// url: "https://example.com/feedback".to_string(), @@ -2639,8 +2704,8 @@ impl TryFrom for CreateElicitati try_from = "CreateElicitationRequestParamDeserializeHelper" )] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")] -pub enum CreateElicitationRequestParams { +#[non_exhaustive] +pub enum ElicitRequestParams { #[serde(rename = "form", rename_all = "camelCase")] FormElicitationParams { /// Protocol-level metadata for this request (SEP-1319) @@ -2674,24 +2739,27 @@ pub enum CreateElicitationRequestParams { }, } -impl RequestParamsMeta for CreateElicitationRequestParams { +impl RequestParamsMeta for ElicitRequestParams { fn meta(&self) -> Option<&Meta> { match self { - CreateElicitationRequestParams::FormElicitationParams { meta, .. } => meta.as_ref(), - CreateElicitationRequestParams::UrlElicitationParams { meta, .. } => meta.as_ref(), + ElicitRequestParams::FormElicitationParams { meta, .. } => meta.as_ref(), + ElicitRequestParams::UrlElicitationParams { meta, .. } => meta.as_ref(), } } fn meta_mut(&mut self) -> &mut Option { match self { - CreateElicitationRequestParams::FormElicitationParams { meta, .. } => meta, - CreateElicitationRequestParams::UrlElicitationParams { meta, .. } => meta, + ElicitRequestParams::FormElicitationParams { meta, .. } => meta, + ElicitRequestParams::UrlElicitationParams { meta, .. } => meta, } } } -/// Deprecated: Use [`CreateElicitationRequestParams`] instead (SEP-1319 compliance). -#[deprecated(since = "0.13.0", note = "Use CreateElicitationRequestParams instead")] -pub type CreateElicitationRequestParam = CreateElicitationRequestParams; +/// Deprecated: Use [`ElicitRequestParams`] instead (SEP-1319 compliance). +#[deprecated(since = "0.13.0", note = "Use ElicitRequestParams instead")] +pub type CreateElicitationRequestParam = ElicitRequestParams; + +#[deprecated(since = "2.0.0", note = "Renamed to ElicitRequestParams")] +pub type CreateElicitationRequestParams = ElicitRequestParams; /// The result returned by a client in response to an elicitation request. /// @@ -2700,8 +2768,8 @@ pub type CreateElicitationRequestParam = CreateElicitationRequestParams; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] -pub struct CreateElicitationResult { +#[non_exhaustive] +pub struct ElicitResult { /// The user's decision on how to handle the elicitation request pub action: ElicitationAction, @@ -2716,8 +2784,8 @@ pub struct CreateElicitationResult { pub meta: Option, } -impl CreateElicitationResult { - /// Create a new CreateElicitationResult. +impl ElicitResult { + /// Create a new ElicitResult. pub fn new(action: ElicitationAction) -> Self { Self { action, @@ -2739,17 +2807,24 @@ impl CreateElicitationResult { } } +#[deprecated(since = "2.0.0", note = "Renamed to ElicitResult")] +pub type CreateElicitationResult = ElicitResult; + /// Request type for creating an elicitation to gather user input -pub type CreateElicitationRequest = - Request; +pub type ElicitRequest = Request; + +#[deprecated(since = "2.0.0", note = "Renamed to ElicitRequest")] +pub type CreateElicitationRequest = ElicitRequest; /// Notification parameters for an url elicitation completion notification. #[derive(Default, Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] +#[non_exhaustive] pub struct ElicitationResponseNotificationParam { pub elicitation_id: String, + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option, } impl ElicitationResponseNotificationParam { @@ -2757,14 +2832,18 @@ impl ElicitationResponseNotificationParam { pub fn new(elicitation_id: impl Into) -> Self { Self { elicitation_id: elicitation_id.into(), + meta: None, } } } /// Notification sent when an url elicitation process is completed. -pub type ElicitationCompletionNotification = +pub type ElicitationCompleteNotification = Notification; +#[deprecated(since = "2.0.0", note = "Renamed to ElicitationCompleteNotification")] +pub type ElicitationCompletionNotification = ElicitationCompleteNotification; + // ============================================================================= // TOOL EXECUTION RESULTS // ============================================================================= @@ -2780,7 +2859,7 @@ pub type ElicitationCompletionNotification = pub struct CallToolResult { /// The content returned by the tool (text, images, etc.) #[serde(default)] - pub content: Vec, + pub content: Vec, /// An optional JSON object that represents the structured result of the tool call #[serde(skip_serializing_if = "Option::is_none")] pub structured_content: Option, @@ -2805,7 +2884,7 @@ impl<'de> Deserialize<'de> for CallToolResult { #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct Helper { - content: Option>, + content: Option>, structured_content: Option, is_error: Option, #[serde(rename = "_meta")] @@ -2836,7 +2915,7 @@ impl<'de> Deserialize<'de> for CallToolResult { impl CallToolResult { /// Create a successful tool result with unstructured content - pub fn success(content: Vec) -> Self { + pub fn success(content: Vec) -> Self { CallToolResult { content, structured_content: None, @@ -2883,17 +2962,17 @@ impl CallToolResult { /// // Tool ran, no result. Caller should see the explanation: /// let rows = run_query(query).await; /// if rows.is_empty() { - /// return Ok(CallToolResult::error(vec![Content::text( + /// return Ok(CallToolResult::error(vec![ContentBlock::text( /// format!("no rows matched '{query}'"), /// )])); /// } /// - /// Ok(CallToolResult::success(vec![Content::text(format_rows(&rows))])) + /// Ok(CallToolResult::success(vec![ContentBlock::text(format_rows(&rows))])) /// } /// # async fn run_query(_: &str) -> Vec<&'static str> { vec![] } /// # fn format_rows(_: &[&str]) -> String { String::new() } /// ``` - pub fn error(content: Vec) -> Self { + pub fn error(content: Vec) -> Self { CallToolResult { content, structured_content: None, @@ -2917,7 +2996,7 @@ impl CallToolResult { /// ``` pub fn structured(value: Value) -> Self { CallToolResult { - content: vec![Content::text(value.to_string())], + content: vec![ContentBlock::text(value.to_string())], structured_content: Some(value), is_error: Some(false), meta: None, @@ -2943,7 +3022,7 @@ impl CallToolResult { /// ``` pub fn structured_error(value: Value) -> Self { CallToolResult { - content: vec![Content::text(value.to_string())], + content: vec![ContentBlock::text(value.to_string())], structured_content: Some(value), is_error: Some(true), meta: None, @@ -3018,7 +3097,7 @@ pub struct CallToolRequestParams { pub arguments: Option, /// Task metadata for async task management (SEP-1319) #[serde(skip_serializing_if = "Option::is_none")] - pub task: Option, + pub task: Option, } impl CallToolRequestParams { @@ -3039,7 +3118,7 @@ impl CallToolRequestParams { } /// Sets the task metadata for this tool call. - pub fn with_task(mut self, task: JsonObject) -> Self { + pub fn with_task(mut self, task: TaskMetadata) -> Self { self.task = Some(task); self } @@ -3055,10 +3134,10 @@ impl RequestParamsMeta for CallToolRequestParams { } impl TaskAugmentedRequestParamsMeta for CallToolRequestParams { - fn task(&self) -> Option<&JsonObject> { + fn task(&self) -> Option<&TaskMetadata> { self.task.as_ref() } - fn task_mut(&mut self) -> &mut Option { + fn task_mut(&mut self) -> &mut Option { &mut self.task } } @@ -3134,6 +3213,8 @@ pub struct GetPromptResult { #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, pub messages: Vec, + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option, } impl GetPromptResult { @@ -3142,6 +3223,7 @@ impl GetPromptResult { Self { description: None, messages, + meta: None, } } @@ -3156,21 +3238,35 @@ impl GetPromptResult { // TASK MANAGEMENT // ============================================================================= -const_string!(GetTaskInfoMethod = "tasks/get"); -pub type GetTaskInfoRequest = Request; +const_string!(GetTaskMethod = "tasks/get"); +pub type GetTaskRequest = Request; + +#[deprecated(since = "2.0.0", note = "Renamed to GetTaskMethod")] +pub type GetTaskInfoMethod = GetTaskMethod; +#[deprecated(since = "2.0.0", note = "Renamed to GetTaskRequest")] +pub type GetTaskInfoRequest = GetTaskRequest; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] -pub struct GetTaskInfoParams { +#[non_exhaustive] +pub struct GetTaskParams { /// Protocol-level metadata for this request (SEP-1319) #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, pub task_id: String, } -impl RequestParamsMeta for GetTaskInfoParams { +impl GetTaskParams { + pub fn new(task_id: impl Into) -> Self { + Self { + meta: None, + task_id: task_id.into(), + } + } +} + +impl RequestParamsMeta for GetTaskParams { fn meta(&self) -> Option<&Meta> { self.meta.as_ref() } @@ -3179,28 +3275,44 @@ impl RequestParamsMeta for GetTaskInfoParams { } } -/// Deprecated: Use [`GetTaskInfoParams`] instead (SEP-1319 compliance). -#[deprecated(since = "0.13.0", note = "Use GetTaskInfoParams instead")] -pub type GetTaskInfoParam = GetTaskInfoParams; +#[deprecated(since = "2.0.0", note = "Renamed to GetTaskParams")] +pub type GetTaskInfoParams = GetTaskParams; + +#[deprecated(since = "0.13.0", note = "Use GetTaskParams instead")] +pub type GetTaskInfoParam = GetTaskParams; const_string!(ListTasksMethod = "tasks/list"); pub type ListTasksRequest = RequestOptionalParam; -const_string!(GetTaskResultMethod = "tasks/result"); -pub type GetTaskResultRequest = Request; +const_string!(GetTaskPayloadMethod = "tasks/result"); +pub type GetTaskPayloadRequest = Request; + +#[deprecated(since = "2.0.0", note = "Renamed to GetTaskPayloadMethod")] +pub type GetTaskResultMethod = GetTaskPayloadMethod; +#[deprecated(since = "2.0.0", note = "Renamed to GetTaskPayloadRequest")] +pub type GetTaskResultRequest = GetTaskPayloadRequest; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] -pub struct GetTaskResultParams { +#[non_exhaustive] +pub struct GetTaskPayloadParams { /// Protocol-level metadata for this request (SEP-1319) #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, pub task_id: String, } -impl RequestParamsMeta for GetTaskResultParams { +impl GetTaskPayloadParams { + pub fn new(task_id: impl Into) -> Self { + Self { + meta: None, + task_id: task_id.into(), + } + } +} + +impl RequestParamsMeta for GetTaskPayloadParams { fn meta(&self) -> Option<&Meta> { self.meta.as_ref() } @@ -3209,9 +3321,10 @@ impl RequestParamsMeta for GetTaskResultParams { } } -/// Deprecated: Use [`GetTaskResultParams`] instead (SEP-1319 compliance). -#[deprecated(since = "0.13.0", note = "Use GetTaskResultParams instead")] -pub type GetTaskResultParam = GetTaskResultParams; +#[deprecated(since = "2.0.0", note = "Renamed to GetTaskPayloadParams")] +pub type GetTaskResultParams = GetTaskPayloadParams; +#[deprecated(since = "2.0.0", note = "Renamed to GetTaskPayloadParams")] +pub type GetTaskResultParam = GetTaskPayloadParams; const_string!(CancelTaskMethod = "tasks/cancel"); pub type CancelTaskRequest = Request; @@ -3219,7 +3332,7 @@ pub type CancelTaskRequest = Request; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] +#[non_exhaustive] pub struct CancelTaskParams { /// Protocol-level metadata for this request (SEP-1319) #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] @@ -3227,6 +3340,15 @@ pub struct CancelTaskParams { pub task_id: String, } +impl CancelTaskParams { + pub fn new(task_id: impl Into) -> Self { + Self { + meta: None, + task_id: task_id.into(), + } + } +} + impl RequestParamsMeta for CancelTaskParams { fn meta(&self) -> Option<&Meta> { self.meta.as_ref() @@ -3239,6 +3361,19 @@ impl RequestParamsMeta for CancelTaskParams { /// Deprecated: Use [`CancelTaskParams`] instead (SEP-1319 compliance). #[deprecated(since = "0.13.0", note = "Use CancelTaskParams instead")] pub type CancelTaskParam = CancelTaskParams; + +// --------------------------------------------------------------------------- +// Task status notification (spec `notifications/tasks/status`) +// --------------------------------------------------------------------------- +const_string!(TaskStatusNotificationMethod = "notifications/tasks/status"); + +/// Parameters for a task status notification (spec `TaskStatusNotificationParams`). +/// +/// The task fields are flattened at the top level: `NotificationParams & Task`. +pub type TaskStatusNotificationParam = crate::model::Task; + +pub type TaskStatusNotification = + Notification; /// Deprecated: Use [`GetTaskResult`] instead (spec alignment). #[deprecated(since = "0.15.0", note = "Use GetTaskResult instead")] pub type GetTaskInfoResult = GetTaskResult; @@ -3251,17 +3386,16 @@ pub struct ListTasksResult { pub tasks: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub next_cursor: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub total: Option, + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option, } impl ListTasksResult { - /// Create a new ListTasksResult. pub fn new(tasks: Vec) -> Self { Self { tasks, next_cursor: None, - total: None, + meta: None, } } } @@ -3335,9 +3469,9 @@ ts_union!( | UnsubscribeRequest | CallToolRequest | ListToolsRequest - | GetTaskInfoRequest + | GetTaskRequest | ListTasksRequest - | GetTaskResultRequest + | GetTaskPayloadRequest | CancelTaskRequest | CustomRequest; ); @@ -3358,9 +3492,9 @@ impl ClientRequest { ClientRequest::UnsubscribeRequest(r) => r.method.as_str(), ClientRequest::CallToolRequest(r) => r.method.as_str(), ClientRequest::ListToolsRequest(r) => r.method.as_str(), - ClientRequest::GetTaskInfoRequest(r) => r.method.as_str(), + ClientRequest::GetTaskRequest(r) => r.method.as_str(), ClientRequest::ListTasksRequest(r) => r.method.as_str(), - ClientRequest::GetTaskResultRequest(r) => r.method.as_str(), + ClientRequest::GetTaskPayloadRequest(r) => r.method.as_str(), ClientRequest::CancelTaskRequest(r) => r.method.as_str(), ClientRequest::CustomRequest(r) => r.method.as_str(), } @@ -3373,6 +3507,7 @@ ts_union!( | ProgressNotification | InitializedNotification | RootsListChangedNotification + | TaskStatusNotification | CustomNotification; ); @@ -3380,7 +3515,7 @@ ts_union!( export type ClientResult = box CreateMessageResult | ListRootsResult - | CreateElicitationResult + | ElicitResult | EmptyResult | CustomResult; ); @@ -3398,7 +3533,7 @@ ts_union!( | PingRequest | CreateMessageRequest | ListRootsRequest - | CreateElicitationRequest + | ElicitRequest | CustomRequest; ); @@ -3411,7 +3546,8 @@ ts_union!( | ResourceListChangedNotification | ToolListChangedNotification | PromptListChangedNotification - | ElicitationCompletionNotification + | ElicitationCompleteNotification + | TaskStatusNotification | CustomNotification; ); @@ -3425,7 +3561,7 @@ ts_union!( | ListResourceTemplatesResult | ReadResourceResult | ListToolsResult - | CreateElicitationResult + | ElicitResult | CreateTaskResult | ListTasksResult | GetTaskResult @@ -3704,6 +3840,7 @@ mod tests { capabilities, server_info, instructions, + .. }) => { assert_eq!(capabilities.logging.unwrap().len(), 0); assert_eq!(capabilities.prompts.unwrap().list_changed, Some(true)); @@ -3924,6 +4061,7 @@ mod tests { website_url: Some("https://docs.example.com".to_string()), }, instructions: None, + meta: None, }; let json = serde_json::to_value(&init_result).unwrap(); @@ -3952,9 +4090,9 @@ mod tests { "required": ["name", "age"] } }); - let elicitation: CreateElicitationRequestParams = + let elicitation: ElicitRequestParams = serde_json::from_value(json_data_without_tag).expect("Deserialization failed"); - if let CreateElicitationRequestParams::FormElicitationParams { + if let ElicitRequestParams::FormElicitationParams { meta, message, requested_schema, @@ -3985,9 +4123,9 @@ mod tests { "required": ["name", "age"] } }); - let elicitation_form: CreateElicitationRequestParams = + let elicitation_form: ElicitRequestParams = serde_json::from_value(json_data_form).expect("Deserialization failed"); - if let CreateElicitationRequestParams::FormElicitationParams { + if let ElicitRequestParams::FormElicitationParams { meta, message, requested_schema, @@ -4011,9 +4149,9 @@ mod tests { "url": "https://example.com/form", "elicitationId": "elicitation-123" }); - let elicitation_url: CreateElicitationRequestParams = + let elicitation_url: ElicitRequestParams = serde_json::from_value(json_data_url).expect("Deserialization failed"); - if let CreateElicitationRequestParams::UrlElicitationParams { + if let ElicitRequestParams::UrlElicitationParams { meta, message, url, @@ -4034,7 +4172,7 @@ mod tests { #[test] fn test_elicitation_serialization() { - let form_elicitation = CreateElicitationRequestParams::FormElicitationParams { + let form_elicitation = ElicitRequestParams::FormElicitationParams { meta: Some(Meta(object!({ "meta_form_key_1": "meta form value 1" }))), message: "Please provide more details.".to_string(), requested_schema: ElicitationSchema::builder() @@ -4058,7 +4196,7 @@ mod tests { }); assert_eq!(json_form, expected_form_json); - let url_elicitation = CreateElicitationRequestParams::UrlElicitationParams { + let url_elicitation = ElicitRequestParams::UrlElicitationParams { meta: Some(Meta(object!({ "meta_url_key_1": "meta url value 1" }))), message: "Please fill out the form at the following URL.".to_string(), url: "https://example.com/form".to_string(), diff --git a/crates/rmcp/src/model/annotated.rs b/crates/rmcp/src/model/annotated.rs index e2e750824..06800d1f9 100644 --- a/crates/rmcp/src/model/annotated.rs +++ b/crates/rmcp/src/model/annotated.rs @@ -1,13 +1,15 @@ -use std::ops::{Deref, DerefMut}; +//! Annotations for content blocks and resources. +//! +//! The `Annotations` struct carries optional hints about audience, priority, and freshness. +//! Individual content/resource types embed `annotations: Option` directly. use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use super::{ - RawAudioContent, RawContent, RawEmbeddedResource, RawImageContent, RawResource, - RawResourceTemplate, RawTextContent, Role, -}; +use super::Role; +/// Optional annotations for the client. The client can use annotations to inform how objects are +/// used or displayed. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] @@ -35,192 +37,23 @@ impl Annotations { audience: None, } } -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] -pub struct Annotated { - #[serde(flatten)] - pub raw: T, - #[serde(skip_serializing_if = "Option::is_none")] - pub annotations: Option, -} -impl Deref for Annotated { - type Target = T; - fn deref(&self) -> &Self::Target { - &self.raw + pub fn with_audience(mut self, audience: Vec) -> Self { + self.audience = Some(audience); + self } -} -impl DerefMut for Annotated { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.raw + pub fn with_priority(mut self, priority: f32) -> Self { + self.priority = Some(priority); + self } -} -impl Annotated { - pub fn new(raw: T, annotations: Option) -> Self { - Self { raw, annotations } - } - pub fn remove_annotation(&mut self) -> Option { - self.annotations.take() - } - pub fn audience(&self) -> Option<&Vec> { - self.annotations.as_ref().and_then(|a| a.audience.as_ref()) - } - pub fn priority(&self) -> Option { - self.annotations.as_ref().and_then(|a| a.priority) - } - pub fn timestamp(&self) -> Option> { - self.annotations.as_ref().and_then(|a| a.last_modified) - } - pub fn with_audience(self, audience: Vec) -> Annotated - where - Self: Sized, - { - if let Some(annotations) = self.annotations { - Annotated { - raw: self.raw, - annotations: Some(Annotations { - audience: Some(audience), - ..annotations - }), - } - } else { - Annotated { - raw: self.raw, - annotations: Some(Annotations { - audience: Some(audience), - priority: None, - last_modified: None, - }), - } - } - } - pub fn with_priority(self, priority: f32) -> Annotated - where - Self: Sized, - { - if let Some(annotations) = self.annotations { - Annotated { - raw: self.raw, - annotations: Some(Annotations { - priority: Some(priority), - ..annotations - }), - } - } else { - Annotated { - raw: self.raw, - annotations: Some(Annotations { - priority: Some(priority), - last_modified: None, - audience: None, - }), - } - } - } - pub fn with_timestamp(self, timestamp: DateTime) -> Annotated - where - Self: Sized, - { - if let Some(annotations) = self.annotations { - Annotated { - raw: self.raw, - annotations: Some(Annotations { - last_modified: Some(timestamp), - ..annotations - }), - } - } else { - Annotated { - raw: self.raw, - annotations: Some(Annotations { - last_modified: Some(timestamp), - priority: None, - audience: None, - }), - } - } - } - pub fn with_timestamp_now(self) -> Annotated - where - Self: Sized, - { - self.with_timestamp(Utc::now()) + pub fn with_timestamp(mut self, timestamp: DateTime) -> Self { + self.last_modified = Some(timestamp); + self } -} - -mod sealed { - pub trait Sealed {} -} -macro_rules! annotate { - ($T: ident) => { - impl sealed::Sealed for $T {} - impl AnnotateAble for $T {} - }; -} - -annotate!(RawContent); -annotate!(RawTextContent); -annotate!(RawImageContent); -annotate!(RawAudioContent); -annotate!(RawEmbeddedResource); -annotate!(RawResource); -annotate!(RawResourceTemplate); -pub trait AnnotateAble: sealed::Sealed { - fn optional_annotate(self, annotations: Option) -> Annotated - where - Self: Sized, - { - Annotated::new(self, annotations) - } - fn annotate(self, annotations: Annotations) -> Annotated - where - Self: Sized, - { - Annotated::new(self, Some(annotations)) - } - fn no_annotation(self) -> Annotated - where - Self: Sized, - { - Annotated::new(self, None) - } - fn with_audience(self, audience: Vec) -> Annotated - where - Self: Sized, - { - self.annotate(Annotations { - audience: Some(audience), - ..Default::default() - }) - } - fn with_priority(self, priority: f32) -> Annotated - where - Self: Sized, - { - self.annotate(Annotations { - priority: Some(priority), - ..Default::default() - }) - } - fn with_timestamp(self, timestamp: DateTime) -> Annotated - where - Self: Sized, - { - self.annotate(Annotations { - last_modified: Some(timestamp), - ..Default::default() - }) - } - fn with_timestamp_now(self) -> Annotated - where - Self: Sized, - { + pub fn with_timestamp_now(self) -> Self { self.with_timestamp(Utc::now()) } } diff --git a/crates/rmcp/src/model/capabilities.rs b/crates/rmcp/src/model/capabilities.rs index 1d32f975a..b42e40c67 100644 --- a/crates/rmcp/src/model/capabilities.rs +++ b/crates/rmcp/src/model/capabilities.rs @@ -34,7 +34,7 @@ pub type ExtensionCapabilities = BTreeMap; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] +#[non_exhaustive] pub struct PromptsCapability { #[serde(skip_serializing_if = "Option::is_none")] pub list_changed: Option, @@ -43,7 +43,7 @@ pub struct PromptsCapability { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] +#[non_exhaustive] pub struct ResourcesCapability { #[serde(skip_serializing_if = "Option::is_none")] pub subscribe: Option, @@ -54,7 +54,7 @@ pub struct ResourcesCapability { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] +#[non_exhaustive] pub struct ToolsCapability { #[serde(skip_serializing_if = "Option::is_none")] pub list_changed: Option, @@ -66,7 +66,7 @@ pub struct ToolsCapability { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] +#[non_exhaustive] pub struct RootsCapabilities { #[serde(skip_serializing_if = "Option::is_none")] pub list_changed: Option, @@ -76,7 +76,7 @@ pub struct RootsCapabilities { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] +#[non_exhaustive] pub struct TasksCapability { #[serde(skip_serializing_if = "Option::is_none")] pub requests: Option, @@ -90,7 +90,7 @@ pub struct TasksCapability { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] +#[non_exhaustive] pub struct TaskRequestsCapability { #[serde(skip_serializing_if = "Option::is_none")] pub sampling: Option, @@ -106,7 +106,7 @@ pub struct TaskRequestsCapability { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] +#[non_exhaustive] pub struct SamplingTaskCapability { #[serde(skip_serializing_if = "Option::is_none")] pub create_message: Option, @@ -115,7 +115,7 @@ pub struct SamplingTaskCapability { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] +#[non_exhaustive] pub struct ElicitationTaskCapability { #[serde(skip_serializing_if = "Option::is_none")] pub create: Option, @@ -124,7 +124,7 @@ pub struct ElicitationTaskCapability { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] +#[non_exhaustive] pub struct ToolsTaskCapability { #[serde(skip_serializing_if = "Option::is_none")] pub call: Option, @@ -205,7 +205,7 @@ impl TasksCapability { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] +#[non_exhaustive] pub struct FormElicitationCapability { /// Whether the client supports JSON Schema validation for elicitation responses. /// When true, the client will validate user input against the requested_schema @@ -214,19 +214,36 @@ pub struct FormElicitationCapability { pub schema_validation: Option, } +impl FormElicitationCapability { + pub fn new() -> Self { + Self::default() + } + + pub fn with_schema_validation(mut self, enabled: bool) -> Self { + self.schema_validation = Some(enabled); + self + } +} + /// Capability for URL mode elicitation. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] +#[non_exhaustive] pub struct UrlElicitationCapability {} +impl UrlElicitationCapability { + pub fn new() -> Self { + Self::default() + } +} + /// Elicitation allows servers to request interactive input from users during tool execution. /// This capability indicates that a client can handle elicitation requests and present /// appropriate UI to users for collecting the requested information. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] +#[non_exhaustive] pub struct ElicitationCapability { /// Whether client supports form-based elicitation. #[serde(skip_serializing_if = "Option::is_none")] @@ -236,6 +253,22 @@ pub struct ElicitationCapability { pub url: Option, } +impl ElicitationCapability { + pub fn new() -> Self { + Self::default() + } + + pub fn with_form(mut self, form: FormElicitationCapability) -> Self { + self.form = Some(form); + self + } + + pub fn with_url(mut self, url: UrlElicitationCapability) -> Self { + self.url = Some(url); + self + } +} + /// Sampling capability with optional sub-capabilities (SEP-1577). /// /// Deprecated by SEP-2577; remains functional and will be removed in a future @@ -244,7 +277,7 @@ pub struct ElicitationCapability { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] +#[non_exhaustive] pub struct SamplingCapability { /// Support for `tools` and `toolChoice` parameters #[serde(skip_serializing_if = "Option::is_none")] diff --git a/crates/rmcp/src/model/content.rs b/crates/rmcp/src/model/content.rs index 7054e2b0e..52887e250 100644 --- a/crates/rmcp/src/model/content.rs +++ b/crates/rmcp/src/model/content.rs @@ -1,78 +1,177 @@ -//! Content sent around agents, extensions, and LLMs -//! The various content types can be display to humans but also understood by models -//! They include optional annotations used to help inform agent usage +//! Content types that flow between agents, tools, prompts, and LLMs. +//! +//! The core union is [`ContentBlock`] (text | image | audio | resource_link | resource), +//! matching the MCP 2025-11-25 `ContentBlock` definition. Each variant carries optional +//! [`Annotations`] and `_meta` inline. +//! +//! [`SamplingMessageContentBlock`] extends the union with `tool_use` and `tool_result` +//! variants for sampling messages (SEP-1577). + use serde::{Deserialize, Serialize}; use serde_json::json; -use super::{AnnotateAble, Annotated, resource::ResourceContents}; +use super::{Annotations, Meta, resource::ResourceContents}; + +// --------------------------------------------------------------------------- +// Flat content structs +// --------------------------------------------------------------------------- +/// Text content block (spec `TextContent`). #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] -pub struct RawTextContent { +#[non_exhaustive] +pub struct TextContent { + /// The text content of the message. pub text: String, - /// Optional protocol-level metadata for this content block + /// Optional protocol-level metadata for this content block. #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, + pub meta: Option, + /// Optional annotations describing how the client should use this content. + #[serde(skip_serializing_if = "Option::is_none")] + pub annotations: Option, +} + +impl TextContent { + pub fn new(text: impl Into) -> Self { + Self { + text: text.into(), + meta: None, + annotations: None, + } + } + + pub fn with_meta(mut self, meta: Meta) -> Self { + self.meta = Some(meta); + self + } + + pub fn with_annotations(mut self, annotations: Annotations) -> Self { + self.annotations = Some(annotations); + self + } } -pub type TextContent = Annotated; + +/// Image content with base64-encoded data (spec `ImageContent`). #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] -pub struct RawImageContent { - /// The base64-encoded image +#[non_exhaustive] +pub struct ImageContent { + /// The base64-encoded image data. pub data: String, + /// The MIME type of the image (e.g. `image/png`). pub mime_type: String, - /// Optional protocol-level metadata for this content block + /// Optional protocol-level metadata for this content block. #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, + pub meta: Option, + /// Optional annotations describing how the client should use this content. + #[serde(skip_serializing_if = "Option::is_none")] + pub annotations: Option, } -pub type ImageContent = Annotated; +impl ImageContent { + pub fn new(data: impl Into, mime_type: impl Into) -> Self { + Self { + data: data.into(), + mime_type: mime_type.into(), + meta: None, + annotations: None, + } + } + + pub fn with_meta(mut self, meta: Meta) -> Self { + self.meta = Some(meta); + self + } + + pub fn with_annotations(mut self, annotations: Annotations) -> Self { + self.annotations = Some(annotations); + self + } +} + +/// Audio content with base64-encoded data (spec `AudioContent`). #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] -pub struct RawEmbeddedResource { - /// Optional protocol-level metadata for this content block +#[non_exhaustive] +pub struct AudioContent { + /// The base64-encoded audio data. + pub data: String, + /// The MIME type of the audio (e.g. `audio/wav`). + pub mime_type: String, + /// Optional protocol-level metadata for this content block. #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, - pub resource: ResourceContents, + pub meta: Option, + /// Optional annotations describing how the client should use this content. + #[serde(skip_serializing_if = "Option::is_none")] + pub annotations: Option, } -impl RawEmbeddedResource { - /// Create a new RawEmbeddedResource. - pub fn new(resource: ResourceContents) -> Self { +impl AudioContent { + pub fn new(data: impl Into, mime_type: impl Into) -> Self { Self { + data: data.into(), + mime_type: mime_type.into(), meta: None, - resource, + annotations: None, } } + + pub fn with_meta(mut self, meta: Meta) -> Self { + self.meta = Some(meta); + self + } + + pub fn with_annotations(mut self, annotations: Annotations) -> Self { + self.annotations = Some(annotations); + self + } } -pub type EmbeddedResource = Annotated; +/// Embedded resource content (spec `EmbeddedResource`). +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[non_exhaustive] +pub struct EmbeddedResource { + /// The embedded resource contents (text or blob). + pub resource: ResourceContents, + /// Optional protocol-level metadata for this content block. + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option, + /// Optional annotations describing how the client should use this content. + #[serde(skip_serializing_if = "Option::is_none")] + pub annotations: Option, +} impl EmbeddedResource { + pub fn new(resource: ResourceContents) -> Self { + Self { + resource, + meta: None, + annotations: None, + } + } + pub fn get_text(&self) -> String { match &self.resource { ResourceContents::TextResourceContents { text, .. } => text.clone(), _ => String::new(), } } -} -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] -pub struct RawAudioContent { - pub data: String, - pub mime_type: String, -} + pub fn with_meta(mut self, meta: Meta) -> Self { + self.meta = Some(meta); + self + } -pub type AudioContent = Annotated; + pub fn with_annotations(mut self, annotations: Annotations) -> Self { + self.annotations = Some(annotations); + self + } +} /// Tool call request from assistant (SEP-1577). #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -80,15 +179,11 @@ pub type AudioContent = Annotated; #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[non_exhaustive] pub struct ToolUseContent { - /// Unique identifier for this tool call pub id: String, - /// Name of the tool to call pub name: String, - /// Input arguments for the tool pub input: super::JsonObject, - /// Optional metadata (preserved for caching) #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, + pub meta: Option, } /// Tool execution result in user message (SEP-1577). @@ -97,18 +192,13 @@ pub struct ToolUseContent { #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[non_exhaustive] pub struct ToolResultContent { - /// Optional metadata #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, - /// ID of the corresponding tool use + pub meta: Option, pub tool_use_id: String, - /// Content blocks returned by the tool #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub content: Vec, - /// Optional structured result + pub content: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub structured_content: Option, - /// Whether tool execution failed #[serde(skip_serializing_if = "Option::is_none")] pub is_error: Option, } @@ -125,7 +215,7 @@ impl ToolUseContent { } impl ToolResultContent { - pub fn new(tool_use_id: impl Into, content: Vec) -> Self { + pub fn new(tool_use_id: impl Into, content: Vec) -> Self { Self { meta: None, tool_use_id: tool_use_id.into(), @@ -135,7 +225,7 @@ impl ToolResultContent { } } - pub fn error(tool_use_id: impl Into, content: Vec) -> Self { + pub fn error(tool_use_id: impl Into, content: Vec) -> Self { Self { meta: None, tool_use_id: tool_use_id.into(), @@ -146,21 +236,26 @@ impl ToolResultContent { } } +// --------------------------------------------------------------------------- +// ContentBlock — the unified content union (spec `ContentBlock`) +// --------------------------------------------------------------------------- + +/// Unified content block union (spec `ContentBlock`). +/// +/// `text | image | audio | resource_link | resource` #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")] -pub enum RawContent { - Text(RawTextContent), - Image(RawImageContent), - Resource(RawEmbeddedResource), - Audio(RawAudioContent), - ResourceLink(super::resource::RawResource), +#[non_exhaustive] +pub enum ContentBlock { + Text(TextContent), + Image(ImageContent), + Audio(AudioContent), + Resource(EmbeddedResource), + ResourceLink(super::resource::Resource), } -pub type Content = Annotated; - -impl RawContent { +impl ContentBlock { pub fn json(json: S) -> Result { let json = serde_json::to_string(&json).map_err(|e| { crate::ErrorData::internal_error( @@ -170,129 +265,106 @@ impl RawContent { )), ) })?; - Ok(RawContent::text(json)) + Ok(ContentBlock::text(json)) } - pub fn text>(text: S) -> Self { - RawContent::Text(RawTextContent { - text: text.into(), - meta: None, - }) + pub fn text(text: impl Into) -> Self { + ContentBlock::Text(TextContent::new(text)) } - pub fn image, T: Into>(data: S, mime_type: T) -> Self { - RawContent::Image(RawImageContent { - data: data.into(), - mime_type: mime_type.into(), - meta: None, - }) + pub fn image(data: impl Into, mime_type: impl Into) -> Self { + ContentBlock::Image(ImageContent::new(data, mime_type)) + } + + pub fn audio(data: impl Into, mime_type: impl Into) -> Self { + ContentBlock::Audio(AudioContent::new(data, mime_type)) } pub fn resource(resource: ResourceContents) -> Self { - RawContent::Resource(RawEmbeddedResource { - meta: None, - resource, - }) + ContentBlock::Resource(EmbeddedResource::new(resource)) } - pub fn embedded_text, T: Into>(uri: S, content: T) -> Self { - RawContent::Resource(RawEmbeddedResource { - meta: None, - resource: ResourceContents::TextResourceContents { + pub fn embedded_text(uri: impl Into, content: impl Into) -> Self { + ContentBlock::Resource(EmbeddedResource::new( + ResourceContents::TextResourceContents { uri: uri.into(), mime_type: Some("text".to_string()), text: content.into(), meta: None, }, - }) + )) + } + + pub fn resource_link(resource: super::resource::Resource) -> Self { + ContentBlock::ResourceLink(resource) } - /// Get the text content if this is a TextContent variant - pub fn as_text(&self) -> Option<&RawTextContent> { + pub fn as_text(&self) -> Option<&TextContent> { match self { - RawContent::Text(text) => Some(text), + ContentBlock::Text(text) => Some(text), _ => None, } } - /// Get the image content if this is an ImageContent variant - pub fn as_image(&self) -> Option<&RawImageContent> { + pub fn as_image(&self) -> Option<&ImageContent> { match self { - RawContent::Image(image) => Some(image), + ContentBlock::Image(image) => Some(image), _ => None, } } - /// Get the resource content if this is an ImageContent variant - pub fn as_resource(&self) -> Option<&RawEmbeddedResource> { + pub fn as_resource(&self) -> Option<&EmbeddedResource> { match self { - RawContent::Resource(resource) => Some(resource), + ContentBlock::Resource(resource) => Some(resource), _ => None, } } - /// Get the resource link if this is a ResourceLink variant - pub fn as_resource_link(&self) -> Option<&super::resource::RawResource> { + pub fn as_resource_link(&self) -> Option<&super::resource::Resource> { match self { - RawContent::ResourceLink(link) => Some(link), + ContentBlock::ResourceLink(link) => Some(link), _ => None, } } - /// Create a resource link content - pub fn resource_link(resource: super::resource::RawResource) -> Self { - RawContent::ResourceLink(resource) + pub fn as_audio(&self) -> Option<&AudioContent> { + match self { + ContentBlock::Audio(audio) => Some(audio), + _ => None, + } } } -impl Content { - pub fn text>(text: S) -> Self { - RawContent::text(text).no_annotation() - } - - pub fn image, T: Into>(data: S, mime_type: T) -> Self { - RawContent::image(data, mime_type).no_annotation() - } - - pub fn resource(resource: ResourceContents) -> Self { - RawContent::resource(resource).no_annotation() - } - - pub fn embedded_text, T: Into>(uri: S, content: T) -> Self { - RawContent::embedded_text(uri, content).no_annotation() - } - - pub fn json(json: S) -> Result { - RawContent::json(json).map(|c| c.no_annotation()) - } - - /// Create a resource link content - pub fn resource_link(resource: super::resource::RawResource) -> Self { - RawContent::resource_link(resource).no_annotation() - } -} +// --------------------------------------------------------------------------- +// JsonContent (unchanged) +// --------------------------------------------------------------------------- #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct JsonContent(S); -/// Types that can be converted into a list of contents + +// --------------------------------------------------------------------------- +// IntoContents +// --------------------------------------------------------------------------- + +/// Types that can be converted into a list of content blocks. pub trait IntoContents { - fn into_contents(self) -> Vec; + fn into_contents(self) -> Vec; } -impl IntoContents for Content { - fn into_contents(self) -> Vec { +impl IntoContents for ContentBlock { + fn into_contents(self) -> Vec { vec![self] } } impl IntoContents for String { - fn into_contents(self) -> Vec { - vec![Content::text(self)] + fn into_contents(self) -> Vec { + vec![ContentBlock::text(self)] } } impl IntoContents for () { - fn into_contents(self) -> Vec { + fn into_contents(self) -> Vec { vec![] } } @@ -305,40 +377,32 @@ mod tests { #[test] fn test_image_content_serialization() { - let image_content = RawImageContent { - data: "base64data".to_string(), - mime_type: "image/png".to_string(), - meta: None, - }; - - let json = serde_json::to_string(&image_content).unwrap(); - println!("ImageContent JSON: {}", json); - - // Verify it contains mimeType (camelCase) not mime_type (snake_case) + let image = ImageContent::new("base64data", "image/png"); + let json = serde_json::to_string(&image).unwrap(); assert!(json.contains("mimeType")); assert!(!json.contains("mime_type")); } #[test] fn test_audio_content_serialization() { - let audio_content = RawAudioContent { - data: "base64audiodata".to_string(), - mime_type: "audio/wav".to_string(), - }; - - let json = serde_json::to_string(&audio_content).unwrap(); - println!("AudioContent JSON: {}", json); - - // Verify it contains mimeType (camelCase) not mime_type (snake_case) + let audio = AudioContent::new("base64audiodata", "audio/wav"); + let json = serde_json::to_string(&audio).unwrap(); assert!(json.contains("mimeType")); assert!(!json.contains("mime_type")); } + #[test] + fn test_audio_content_has_meta() { + let audio = AudioContent::new("data", "audio/wav").with_meta(Meta::default()); + let json = serde_json::to_value(&audio).unwrap(); + assert!(json.get("_meta").is_some()); + } + #[test] fn test_resource_link_serialization() { - use super::super::resource::RawResource; + use super::super::resource::Resource; - let resource_link = RawContent::ResourceLink(RawResource { + let resource_link = ContentBlock::ResourceLink(Resource { uri: "file:///test.txt".to_string(), name: "test.txt".to_string(), title: None, @@ -347,12 +411,10 @@ mod tests { size: Some(100), icons: None, meta: None, + annotations: None, }); let json = serde_json::to_string(&resource_link).unwrap(); - println!("ResourceLink JSON: {}", json); - - // Verify it contains the correct type tag assert!(json.contains("\"type\":\"resource_link\"")); assert!(json.contains("\"uri\":\"file:///test.txt\"")); assert!(json.contains("\"name\":\"test.txt\"")); @@ -368,9 +430,9 @@ mod tests { "mimeType": "text/plain" }"#; - let content: RawContent = serde_json::from_str(json).unwrap(); + let content: ContentBlock = serde_json::from_str(json).unwrap(); - if let RawContent::ResourceLink(resource) = content { + if let ContentBlock::ResourceLink(resource) = content { assert_eq!(resource.uri, "file:///example.txt"); assert_eq!(resource.name, "example.txt"); assert_eq!(resource.description, Some("Example file".to_string())); @@ -379,4 +441,15 @@ mod tests { panic!("Expected ResourceLink variant"); } } + + #[test] + fn test_content_block_text_with_annotations() { + let block = ContentBlock::Text( + TextContent::new("hello").with_annotations(Annotations::default().with_priority(0.8)), + ); + let json = serde_json::to_value(&block).unwrap(); + assert_eq!(json["type"], "text"); + assert_eq!(json["text"], "hello"); + assert_eq!(json["annotations"]["priority"], 0.8_f32); + } } diff --git a/crates/rmcp/src/model/elicitation_schema.rs b/crates/rmcp/src/model/elicitation_schema.rs index 0e8244c46..d2712f463 100644 --- a/crates/rmcp/src/model/elicitation_schema.rs +++ b/crates/rmcp/src/model/elicitation_schema.rs @@ -49,8 +49,8 @@ const_string!(ArrayTypeConst = "array"); #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(untagged)] -#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")] -pub enum PrimitiveSchema { +#[non_exhaustive] +pub enum PrimitiveSchemaDefinition { /// Enum property (explicit enum schema) Enum(EnumSchema), /// String property (with optional enum constraint) @@ -63,6 +63,9 @@ pub enum PrimitiveSchema { Boolean(BooleanSchema), } +#[deprecated(since = "2.0.0", note = "Renamed to PrimitiveSchemaDefinition")] +pub type PrimitiveSchema = PrimitiveSchemaDefinition; + // ============================================================================= // STRING SCHEMA // ============================================================================= @@ -71,7 +74,7 @@ pub enum PrimitiveSchema { #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "kebab-case")] -#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")] +#[non_exhaustive] pub enum StringFormat { /// Email address format Email, @@ -346,7 +349,7 @@ impl NumberSchema { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] +#[non_exhaustive] pub struct IntegerSchema { /// Type discriminator #[serde(rename = "type")] @@ -513,7 +516,7 @@ impl BooleanSchema { /// Represent single entry for titled item #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] +#[non_exhaustive] pub struct ConstTitle { #[serde(rename = "const")] pub const_: String, @@ -534,7 +537,7 @@ impl ConstTitle { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] +#[non_exhaustive] pub struct LegacyEnumSchema { #[serde(rename = "type")] pub type_: StringTypeConst, @@ -546,6 +549,21 @@ pub struct LegacyEnumSchema { pub enum_: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub enum_names: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub default: Option, +} + +impl LegacyEnumSchema { + pub fn new(enum_values: Vec) -> Self { + Self { + type_: StringTypeConst, + title: None, + description: None, + enum_: enum_values, + enum_names: None, + default: None, + } + } } /// Untitled single-select @@ -601,7 +619,7 @@ impl TitledSingleSelectEnumSchema { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(untagged)] -#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")] +#[non_exhaustive] pub enum SingleSelectEnumSchema { Untitled(UntitledSingleSelectEnumSchema), Titled(TitledSingleSelectEnumSchema), @@ -610,7 +628,7 @@ pub enum SingleSelectEnumSchema { /// Items for untitled multi-select options #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] +#[non_exhaustive] pub struct UntitledItems { #[serde(rename = "type")] pub type_: StringTypeConst, @@ -618,10 +636,19 @@ pub struct UntitledItems { pub enum_: Vec, } +impl UntitledItems { + pub fn new(enum_values: Vec) -> Self { + Self { + type_: StringTypeConst, + enum_: enum_values, + } + } +} + /// Items for titled multi-select options #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] +#[non_exhaustive] pub struct TitledItems { // MCP spec requires "anyOf" for multi-select enums (allows any combination) // Alias "oneOf" for compatibility with schemars @@ -727,7 +754,7 @@ impl TitledMultiSelectEnumSchema { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(untagged)] -#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")] +#[non_exhaustive] pub enum MultiSelectEnumSchema { Untitled(UntitledMultiSelectEnumSchema), Titled(TitledMultiSelectEnumSchema), @@ -751,7 +778,7 @@ pub enum MultiSelectEnumSchema { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(untagged)] -#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")] +#[non_exhaustive] pub enum EnumSchema { Single(SingleSelectEnumSchema), Multi(MultiSelectEnumSchema), @@ -1090,7 +1117,7 @@ impl EnumSchema { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] +#[non_exhaustive] pub struct ElicitationSchema { /// Always "object" for elicitation schemas #[serde(rename = "type")] @@ -1101,7 +1128,7 @@ pub struct ElicitationSchema { pub title: Option>, /// Property definitions (must be primitive types) - pub properties: BTreeMap, + pub properties: BTreeMap, /// List of required property names #[serde(skip_serializing_if = "Option::is_none")] @@ -1114,7 +1141,7 @@ pub struct ElicitationSchema { impl ElicitationSchema { /// Create a new elicitation schema with the given properties - pub fn new(properties: BTreeMap) -> Self { + pub fn new(properties: BTreeMap) -> Self { Self { type_: ObjectTypeConst, title: None, @@ -1237,7 +1264,7 @@ impl ElicitationSchema { #[derive(Debug, Default)] #[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct ElicitationSchemaBuilder { - pub properties: BTreeMap, + pub properties: BTreeMap, pub required: Vec, pub title: Option>, pub description: Option>, @@ -1250,13 +1277,17 @@ impl ElicitationSchemaBuilder { } /// Add a property to the schema - pub fn property(mut self, name: impl Into, schema: PrimitiveSchema) -> Self { + pub fn property(mut self, name: impl Into, schema: PrimitiveSchemaDefinition) -> Self { self.properties.insert(name.into(), schema); self } /// Add a required property to the schema - pub fn required_property(mut self, name: impl Into, schema: PrimitiveSchema) -> Self { + pub fn required_property( + mut self, + name: impl Into, + schema: PrimitiveSchemaDefinition, + ) -> Self { let name_str = name.into(); self.required.push(name_str.clone()); self.properties.insert(name_str, schema); @@ -1264,7 +1295,7 @@ impl ElicitationSchemaBuilder { } // =========================================================================== - // TYPED PROPERTY METHODS - Cleaner API without PrimitiveSchema wrapper + // TYPED PROPERTY METHODS - Cleaner API without PrimitiveSchemaDefinition wrapper // =========================================================================== /// Add a string property with custom builder (required) @@ -1273,8 +1304,10 @@ impl ElicitationSchemaBuilder { name: impl Into, f: impl FnOnce(StringSchema) -> StringSchema, ) -> Self { - self.properties - .insert(name.into(), PrimitiveSchema::String(f(StringSchema::new()))); + self.properties.insert( + name.into(), + PrimitiveSchemaDefinition::String(f(StringSchema::new())), + ); self } @@ -1286,8 +1319,10 @@ impl ElicitationSchemaBuilder { ) -> Self { let name_str = name.into(); self.required.push(name_str.clone()); - self.properties - .insert(name_str, PrimitiveSchema::String(f(StringSchema::new()))); + self.properties.insert( + name_str, + PrimitiveSchemaDefinition::String(f(StringSchema::new())), + ); self } @@ -1297,8 +1332,10 @@ impl ElicitationSchemaBuilder { name: impl Into, f: impl FnOnce(NumberSchema) -> NumberSchema, ) -> Self { - self.properties - .insert(name.into(), PrimitiveSchema::Number(f(NumberSchema::new()))); + self.properties.insert( + name.into(), + PrimitiveSchemaDefinition::Number(f(NumberSchema::new())), + ); self } @@ -1310,8 +1347,10 @@ impl ElicitationSchemaBuilder { ) -> Self { let name_str = name.into(); self.required.push(name_str.clone()); - self.properties - .insert(name_str, PrimitiveSchema::Number(f(NumberSchema::new()))); + self.properties.insert( + name_str, + PrimitiveSchemaDefinition::Number(f(NumberSchema::new())), + ); self } @@ -1323,7 +1362,7 @@ impl ElicitationSchemaBuilder { ) -> Self { self.properties.insert( name.into(), - PrimitiveSchema::Integer(f(IntegerSchema::new())), + PrimitiveSchemaDefinition::Integer(f(IntegerSchema::new())), ); self } @@ -1336,8 +1375,10 @@ impl ElicitationSchemaBuilder { ) -> Self { let name_str = name.into(); self.required.push(name_str.clone()); - self.properties - .insert(name_str, PrimitiveSchema::Integer(f(IntegerSchema::new()))); + self.properties.insert( + name_str, + PrimitiveSchemaDefinition::Integer(f(IntegerSchema::new())), + ); self } @@ -1349,7 +1390,7 @@ impl ElicitationSchemaBuilder { ) -> Self { self.properties.insert( name.into(), - PrimitiveSchema::Boolean(f(BooleanSchema::new())), + PrimitiveSchemaDefinition::Boolean(f(BooleanSchema::new())), ); self } @@ -1362,8 +1403,10 @@ impl ElicitationSchemaBuilder { ) -> Self { let name_str = name.into(); self.required.push(name_str.clone()); - self.properties - .insert(name_str, PrimitiveSchema::Boolean(f(BooleanSchema::new()))); + self.properties.insert( + name_str, + PrimitiveSchemaDefinition::Boolean(f(BooleanSchema::new())), + ); self } @@ -1373,22 +1416,28 @@ impl ElicitationSchemaBuilder { /// Add a required string property pub fn required_string(self, name: impl Into) -> Self { - self.required_property(name, PrimitiveSchema::String(StringSchema::new())) + self.required_property(name, PrimitiveSchemaDefinition::String(StringSchema::new())) } /// Add an optional string property pub fn optional_string(self, name: impl Into) -> Self { - self.property(name, PrimitiveSchema::String(StringSchema::new())) + self.property(name, PrimitiveSchemaDefinition::String(StringSchema::new())) } /// Add a required email property pub fn required_email(self, name: impl Into) -> Self { - self.required_property(name, PrimitiveSchema::String(StringSchema::email())) + self.required_property( + name, + PrimitiveSchemaDefinition::String(StringSchema::email()), + ) } /// Add an optional email property pub fn optional_email(self, name: impl Into) -> Self { - self.property(name, PrimitiveSchema::String(StringSchema::email())) + self.property( + name, + PrimitiveSchemaDefinition::String(StringSchema::email()), + ) } /// Add a required string property with custom builder @@ -1397,7 +1446,10 @@ impl ElicitationSchemaBuilder { name: impl Into, f: impl FnOnce(StringSchema) -> StringSchema, ) -> Self { - self.required_property(name, PrimitiveSchema::String(f(StringSchema::new()))) + self.required_property( + name, + PrimitiveSchemaDefinition::String(f(StringSchema::new())), + ) } /// Add an optional string property with custom builder @@ -1406,7 +1458,10 @@ impl ElicitationSchemaBuilder { name: impl Into, f: impl FnOnce(StringSchema) -> StringSchema, ) -> Self { - self.property(name, PrimitiveSchema::String(f(StringSchema::new()))) + self.property( + name, + PrimitiveSchemaDefinition::String(f(StringSchema::new())), + ) } // Convenience methods for numbers @@ -1415,7 +1470,7 @@ impl ElicitationSchemaBuilder { pub fn required_number(self, name: impl Into, min: f64, max: f64) -> Self { self.required_property( name, - PrimitiveSchema::Number(NumberSchema::new().range(min, max)), + PrimitiveSchemaDefinition::Number(NumberSchema::new().range(min, max)), ) } @@ -1423,7 +1478,7 @@ impl ElicitationSchemaBuilder { pub fn optional_number(self, name: impl Into, min: f64, max: f64) -> Self { self.property( name, - PrimitiveSchema::Number(NumberSchema::new().range(min, max)), + PrimitiveSchemaDefinition::Number(NumberSchema::new().range(min, max)), ) } @@ -1433,7 +1488,10 @@ impl ElicitationSchemaBuilder { name: impl Into, f: impl FnOnce(NumberSchema) -> NumberSchema, ) -> Self { - self.required_property(name, PrimitiveSchema::Number(f(NumberSchema::new()))) + self.required_property( + name, + PrimitiveSchemaDefinition::Number(f(NumberSchema::new())), + ) } /// Add an optional number property with custom builder @@ -1442,7 +1500,10 @@ impl ElicitationSchemaBuilder { name: impl Into, f: impl FnOnce(NumberSchema) -> NumberSchema, ) -> Self { - self.property(name, PrimitiveSchema::Number(f(NumberSchema::new()))) + self.property( + name, + PrimitiveSchemaDefinition::Number(f(NumberSchema::new())), + ) } // Convenience methods for integers @@ -1451,7 +1512,7 @@ impl ElicitationSchemaBuilder { pub fn required_integer(self, name: impl Into, min: i64, max: i64) -> Self { self.required_property( name, - PrimitiveSchema::Integer(IntegerSchema::new().range(min, max)), + PrimitiveSchemaDefinition::Integer(IntegerSchema::new().range(min, max)), ) } @@ -1459,7 +1520,7 @@ impl ElicitationSchemaBuilder { pub fn optional_integer(self, name: impl Into, min: i64, max: i64) -> Self { self.property( name, - PrimitiveSchema::Integer(IntegerSchema::new().range(min, max)), + PrimitiveSchemaDefinition::Integer(IntegerSchema::new().range(min, max)), ) } @@ -1469,7 +1530,10 @@ impl ElicitationSchemaBuilder { name: impl Into, f: impl FnOnce(IntegerSchema) -> IntegerSchema, ) -> Self { - self.required_property(name, PrimitiveSchema::Integer(f(IntegerSchema::new()))) + self.required_property( + name, + PrimitiveSchemaDefinition::Integer(f(IntegerSchema::new())), + ) } /// Add an optional integer property with custom builder @@ -1478,21 +1542,27 @@ impl ElicitationSchemaBuilder { name: impl Into, f: impl FnOnce(IntegerSchema) -> IntegerSchema, ) -> Self { - self.property(name, PrimitiveSchema::Integer(f(IntegerSchema::new()))) + self.property( + name, + PrimitiveSchemaDefinition::Integer(f(IntegerSchema::new())), + ) } // Convenience methods for booleans /// Add a required boolean property pub fn required_bool(self, name: impl Into) -> Self { - self.required_property(name, PrimitiveSchema::Boolean(BooleanSchema::new())) + self.required_property( + name, + PrimitiveSchemaDefinition::Boolean(BooleanSchema::new()), + ) } /// Add an optional boolean property with default value pub fn optional_bool(self, name: impl Into, default: bool) -> Self { self.property( name, - PrimitiveSchema::Boolean(BooleanSchema::new().with_default(default)), + PrimitiveSchemaDefinition::Boolean(BooleanSchema::new().with_default(default)), ) } @@ -1502,7 +1572,10 @@ impl ElicitationSchemaBuilder { name: impl Into, f: impl FnOnce(BooleanSchema) -> BooleanSchema, ) -> Self { - self.required_property(name, PrimitiveSchema::Boolean(f(BooleanSchema::new()))) + self.required_property( + name, + PrimitiveSchemaDefinition::Boolean(f(BooleanSchema::new())), + ) } /// Add an optional boolean property with custom builder @@ -1511,19 +1584,22 @@ impl ElicitationSchemaBuilder { name: impl Into, f: impl FnOnce(BooleanSchema) -> BooleanSchema, ) -> Self { - self.property(name, PrimitiveSchema::Boolean(f(BooleanSchema::new()))) + self.property( + name, + PrimitiveSchemaDefinition::Boolean(f(BooleanSchema::new())), + ) } // Enum convenience methods /// Add a required enum property using EnumSchema pub fn required_enum_schema(self, name: impl Into, enum_schema: EnumSchema) -> Self { - self.required_property(name, PrimitiveSchema::Enum(enum_schema)) + self.required_property(name, PrimitiveSchemaDefinition::Enum(enum_schema)) } /// Add an optional enum property using EnumSchema pub fn optional_enum_schema(self, name: impl Into, enum_schema: EnumSchema) -> Self { - self.property(name, PrimitiveSchema::Enum(enum_schema)) + self.property(name, PrimitiveSchemaDefinition::Enum(enum_schema)) } /// Add a required enum property using values. Creates an untitled single-select enum. @@ -1534,12 +1610,13 @@ impl ElicitationSchemaBuilder { pub fn required_enum(self, name: impl Into, values: Vec) -> Self { self.required_property( name, - PrimitiveSchema::Enum(EnumSchema::Legacy(LegacyEnumSchema { + PrimitiveSchemaDefinition::Enum(EnumSchema::Legacy(LegacyEnumSchema { type_: StringTypeConst, title: None, description: None, enum_: values, enum_names: None, + default: None, })), ) } @@ -1552,12 +1629,13 @@ impl ElicitationSchemaBuilder { pub fn optional_enum(self, name: impl Into, values: Vec) -> Self { self.property( name, - PrimitiveSchema::Enum(EnumSchema::Legacy(LegacyEnumSchema { + PrimitiveSchemaDefinition::Enum(EnumSchema::Legacy(LegacyEnumSchema { type_: StringTypeConst, title: None, description: None, enum_: values, enum_names: None, + default: None, })), ) } @@ -1732,6 +1810,7 @@ mod tests { description: Some("A legacy enum schema".into()), enum_: vec!["A".to_string(), "B".to_string()], enum_names: Some(vec!["Option A".to_string(), "Option B".to_string()]), + default: None, }); let json = serde_json::to_value(&schema)?; @@ -1776,6 +1855,7 @@ mod tests { description: None, enum_: vec!["a".to_string(), "b".to_string()], enum_names: None, + default: None, }); let json = serde_json::to_value(&schema)?; assert!(!json.as_object().unwrap().contains_key("enumNames")); @@ -1970,14 +2050,14 @@ mod tests { "type": "string", "enum": ["a", "b"] }); - let schema: PrimitiveSchema = serde_json::from_value(json).unwrap(); - assert!(matches!(schema, PrimitiveSchema::Enum(_))); + let schema: PrimitiveSchemaDefinition = serde_json::from_value(json).unwrap(); + assert!(matches!(schema, PrimitiveSchemaDefinition::Enum(_))); // Test that string schemas deserialize as String variant let json = json!({ "type": "string" }); - let schema: PrimitiveSchema = serde_json::from_value(json).unwrap(); - assert!(matches!(schema, PrimitiveSchema::String(_))); + let schema: PrimitiveSchemaDefinition = serde_json::from_value(json).unwrap(); + assert!(matches!(schema, PrimitiveSchemaDefinition::String(_))); } #[test] diff --git a/crates/rmcp/src/model/meta.rs b/crates/rmcp/src/model/meta.rs index 186db6a24..4c9cd618a 100644 --- a/crates/rmcp/src/model/meta.rs +++ b/crates/rmcp/src/model/meta.rs @@ -5,7 +5,7 @@ use serde_json::Value; use super::{ ClientNotification, ClientRequest, CustomNotification, CustomRequest, Extensions, JsonObject, - JsonRpcMessage, NumberOrString, ProgressToken, ServerNotification, ServerRequest, + JsonRpcMessage, NumberOrString, ProgressToken, ServerNotification, ServerRequest, TaskMetadata, }; pub trait GetMeta { @@ -54,11 +54,11 @@ pub trait RequestParamsMeta { /// can include a `task` field to signal that the caller wants task-augmented execution. pub trait TaskAugmentedRequestParamsMeta: RequestParamsMeta { /// Get a reference to the task field - fn task(&self) -> Option<&JsonObject>; + fn task(&self) -> Option<&TaskMetadata>; /// Get a mutable reference to the task field - fn task_mut(&mut self) -> &mut Option; + fn task_mut(&mut self) -> &mut Option; /// Set the task field - fn set_task(&mut self, task: JsonObject) { + fn set_task(&mut self, task: TaskMetadata) { *self.task_mut() = Some(task); } } @@ -152,9 +152,9 @@ variant_extension! { CallToolRequest ListToolsRequest CustomRequest - GetTaskInfoRequest + GetTaskRequest ListTasksRequest - GetTaskResultRequest + GetTaskPayloadRequest CancelTaskRequest } } @@ -164,7 +164,7 @@ variant_extension! { PingRequest CreateMessageRequest ListRootsRequest - CreateElicitationRequest + ElicitRequest CustomRequest } } @@ -175,6 +175,7 @@ variant_extension! { ProgressNotification InitializedNotification RootsListChangedNotification + TaskStatusNotification CustomNotification } } @@ -188,7 +189,8 @@ variant_extension! { ResourceListChangedNotification ToolListChangedNotification PromptListChangedNotification - ElicitationCompletionNotification + ElicitationCompleteNotification + TaskStatusNotification CustomNotification } } diff --git a/crates/rmcp/src/model/prompt.rs b/crates/rmcp/src/model/prompt.rs index a44183fbd..e438260b5 100644 --- a/crates/rmcp/src/model/prompt.rs +++ b/crates/rmcp/src/model/prompt.rs @@ -1,37 +1,31 @@ use serde::{Deserialize, Serialize}; use super::{ - AnnotateAble, Annotations, Icon, Meta, RawEmbeddedResource, - content::{AudioContent, EmbeddedResource, ImageContent}, + Annotations, ContentBlock, Icon, Meta, Role, + content::{AudioContent, EmbeddedResource, ImageContent, TextContent}, resource::ResourceContents, }; -/// A prompt that can be used to generate text from a model +/// A prompt or prompt template that the server offers (spec `Prompt`). #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[non_exhaustive] 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, - /// Optional arguments that can be passed to customize the prompt #[serde(skip_serializing_if = "Option::is_none")] pub arguments: Option>, - /// 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 { - /// Create a new prompt with the given name, description and arguments pub fn new( name: N, description: Option, @@ -51,7 +45,6 @@ impl Prompt { } } - /// Create a new prompt from raw fields (used by the macro) pub fn from_raw( name: impl Into, description: Option>, @@ -67,45 +60,37 @@ impl Prompt { } } - /// Set the human-readable 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: Vec) -> Self { self.icons = Some(icons); self } - /// Set the metadata pub fn with_meta(mut self, meta: Meta) -> Self { self.meta = Some(meta); self } } -/// Represents a prompt argument that can be passed to customize the prompt +/// Describes an argument that a prompt can accept (spec `PromptArgument`). #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[non_exhaustive] 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, - /// Whether this argument is required #[serde(skip_serializing_if = "Option::is_none")] pub required: Option, } impl PromptArgument { - /// Create a new prompt argument pub fn new>(name: N) -> Self { PromptArgument { name: name.into(), @@ -115,108 +100,51 @@ impl PromptArgument { } } - /// Set the title pub fn with_title>(mut self, title: T) -> Self { self.title = Some(title.into()); self } - /// Set the description pub fn with_description>(mut self, description: D) -> Self { self.description = Some(description.into()); self } - /// Set the required flag pub fn with_required(mut self, required: bool) -> Self { self.required = Some(required); self } } -/// Represents the role of a message sender in a prompt conversation -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")] -pub enum PromptMessageRole { - User, - Assistant, -} - -/// Content types that can be included in prompt messages -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")] -pub enum PromptMessageContent { - /// Plain text content - Text { text: String }, - /// Image content with base64-encoded data - Image { - #[serde(flatten)] - image: ImageContent, - }, - /// Audio content with base64-encoded data - Audio { - #[serde(flatten)] - audio: AudioContent, - }, - /// Embedded server-side resource - Resource { - #[serde(flatten)] - resource: EmbeddedResource, - }, - /// A link to a resource that can be fetched separately - ResourceLink { - #[serde(flatten)] - link: super::resource::Resource, - }, -} - -impl PromptMessageContent { - pub fn text(text: impl Into) -> Self { - Self::Text { text: text.into() } - } - - /// Create a resource link content - pub fn resource_link(resource: super::resource::Resource) -> Self { - Self::ResourceLink { link: resource } - } -} - -/// A message in a prompt conversation +/// A message returned as part of a prompt (spec `PromptMessage`). +/// +/// Uses the unified `ContentBlock` for its content (text | image | audio | resource_link | resource). #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[non_exhaustive] pub struct PromptMessage { - /// The role of the message sender - pub role: PromptMessageRole, - /// The content of the message - pub content: PromptMessageContent, + pub role: Role, + pub content: ContentBlock, } impl PromptMessage { - /// Create a new prompt message with the given role and content - pub fn new(role: PromptMessageRole, content: PromptMessageContent) -> Self { + pub fn new(role: Role, content: ContentBlock) -> Self { Self { role, content } } - /// Create a new text message with the given role and text content - pub fn new_text>(role: PromptMessageRole, text: S) -> Self { + pub fn new_text>(role: Role, text: S) -> Self { Self { role, - content: PromptMessageContent::Text { text: text.into() }, + content: ContentBlock::text(text), } } - /// Create a new image message. `meta` and `annotations` are optional. #[cfg(feature = "base64")] pub fn new_image( - role: PromptMessageRole, + role: Role, data: &[u8], mime_type: &str, - meta: Option, + meta: Option, annotations: Option, ) -> Self { use base64::{Engine, prelude::BASE64_STANDARD}; @@ -224,23 +152,21 @@ impl PromptMessage { let base64 = BASE64_STANDARD.encode(data); Self { role, - content: PromptMessageContent::Image { - image: crate::model::RawImageContent { - data: base64, - mime_type: mime_type.into(), - meta, - } - .optional_annotate(annotations), - }, + content: ContentBlock::Image(ImageContent { + data: base64, + mime_type: mime_type.into(), + meta, + annotations, + }), } } - /// Create a new audio message. `annotations` is optional. #[cfg(feature = "base64")] pub fn new_audio( - role: PromptMessageRole, + role: Role, data: &[u8], mime_type: &str, + meta: Option, annotations: Option, ) -> Self { use base64::{Engine, prelude::BASE64_STANDARD}; @@ -248,24 +174,22 @@ impl PromptMessage { let base64 = BASE64_STANDARD.encode(data); Self { role, - content: PromptMessageContent::Audio { - audio: crate::model::RawAudioContent { - data: base64, - mime_type: mime_type.into(), - } - .optional_annotate(annotations), - }, + content: ContentBlock::Audio(AudioContent { + data: base64, + mime_type: mime_type.into(), + meta, + annotations, + }), } } - /// Create a new resource message. `resource_meta`, `resource_content_meta`, and `annotations` are optional. pub fn new_resource( - role: PromptMessageRole, + role: Role, uri: String, mime_type: Option, text: Option, - resource_meta: Option, - resource_content_meta: Option, + resource_meta: Option, + resource_content_meta: Option, annotations: Option, ) -> Self { let resource_contents = match text { @@ -284,31 +208,29 @@ impl PromptMessage { }; Self { role, - content: PromptMessageContent::Resource { - resource: RawEmbeddedResource { - meta: resource_meta, - resource: resource_contents, - } - .optional_annotate(annotations), - }, + content: ContentBlock::Resource(EmbeddedResource { + meta: resource_meta, + resource: resource_contents, + annotations, + }), } } - /// Note: PromptMessage text content does not carry protocol-level _meta per current schema. - /// This function exists for API symmetry but ignores the meta parameter. - pub fn new_text_with_meta>( - role: PromptMessageRole, - text: S, - _meta: Option, - ) -> Self { - Self::new_text(role, text) + pub fn new_text_with_meta>(role: Role, text: S, meta: Option) -> Self { + Self { + role, + content: ContentBlock::Text(TextContent { + text: text.into(), + meta, + annotations: None, + }), + } } - /// Create a new resource link message - pub fn new_resource_link(role: PromptMessageRole, resource: super::resource::Resource) -> Self { + pub fn new_resource_link(role: Role, resource: super::resource::Resource) -> Self { Self { role, - content: PromptMessageContent::ResourceLink { link: resource }, + content: ContentBlock::ResourceLink(resource), } } } @@ -321,35 +243,15 @@ mod tests { #[test] fn test_prompt_message_image_serialization() { - let image_content = crate::model::RawImageContent { - data: "base64data".to_string(), - mime_type: "image/png".to_string(), - meta: None, - }; - - let json = serde_json::to_string(&image_content).unwrap(); - println!("PromptMessage ImageContent JSON: {}", json); - - // Verify it contains mimeType (camelCase) not mime_type (snake_case) + let image = ImageContent::new("base64data", "image/png"); + let json = serde_json::to_string(&image).unwrap(); assert!(json.contains("mimeType")); assert!(!json.contains("mime_type")); } #[test] fn test_prompt_message_audio_serialization_and_deserialization() { - // Audio is part of the spec's ContentBlock union for prompt messages - // (text | image | audio | resource_link | resource). Ensure the Audio - // variant serializes to the flat, spec-compliant shape - // `{ "type": "audio", "data", "mimeType" }` and parses back. - // See: https://modelcontextprotocol.io/specification/2025-06-18/server/prompts - let content = PromptMessageContent::Audio { - audio: crate::model::RawAudioContent { - data: "YXVkaW8=".to_string(), - mime_type: "audio/wav".to_string(), - } - .no_annotation(), - }; - + let content = ContentBlock::Audio(AudioContent::new("YXVkaW8=", "audio/wav")); let value = serde_json::to_value(&content).unwrap(); assert_eq!(value.get("type").and_then(|v| v.as_str()), Some("audio")); assert_eq!(value.get("data").and_then(|v| v.as_str()), Some("YXVkaW8=")); @@ -359,18 +261,15 @@ mod tests { "expected camelCase mimeType, got: {value:#?}" ); - // Regression: a spec-valid audio content block must deserialize into - // the Audio variant (previously failed with "unknown variant `audio`"). let json = r#"{"type":"audio","data":"YXVkaW8=","mimeType":"audio/wav"}"#; - let parsed: PromptMessageContent = serde_json::from_str(json).unwrap(); + let parsed: ContentBlock = serde_json::from_str(json).unwrap(); assert_eq!(parsed, content); } #[test] #[cfg(feature = "base64")] fn test_prompt_message_new_audio_constructor() { - let message = - PromptMessage::new_audio(PromptMessageRole::User, b"hello", "audio/wav", None); + let message = PromptMessage::new_audio(Role::User, b"hello", "audio/wav", None, None); let value = serde_json::to_value(&message).unwrap(); let content = value.get("content").expect("content present"); assert_eq!(content.get("type").and_then(|v| v.as_str()), Some("audio")); @@ -378,7 +277,6 @@ mod tests { content.get("mimeType").and_then(|v| v.as_str()), Some("audio/wav") ); - // base64 of "hello" assert_eq!( content.get("data").and_then(|v| v.as_str()), Some("aGVsbG8=") @@ -387,16 +285,12 @@ mod tests { #[test] fn test_prompt_message_resource_link_serialization() { - use super::super::resource::RawResource; + use super::super::resource::Resource; - let resource = RawResource::new("file:///test.txt", "test.txt"); - let message = - PromptMessage::new_resource_link(PromptMessageRole::User, resource.no_annotation()); + let resource = Resource::new("file:///test.txt", "test.txt"); + let message = PromptMessage::new_resource_link(Role::User, resource); let json = serde_json::to_string(&message).unwrap(); - println!("PromptMessage with ResourceLink JSON: {}", json); - - // Verify it contains the correct type tag assert!(json.contains("\"type\":\"resource_link\"")); assert!(json.contains("\"uri\":\"file:///test.txt\"")); assert!(json.contains("\"name\":\"test.txt\"")); @@ -404,12 +298,8 @@ mod tests { #[test] fn test_prompt_message_resource_serialization_is_flat() { - // Regression test: PromptMessageContent::Resource must serialize to - // the spec-compliant flat shape `{ "type": "resource", "resource": { "uri", "mimeType", "text" } }` - // and NOT the double-nested shape `{ "type": "resource", "resource": { "resource": {...} } }`. - // See: https://modelcontextprotocol.io/specification/2025-06-18/server/prompts let message = PromptMessage::new_resource( - PromptMessageRole::User, + Role::User, "alc://packages/sc/narrative".to_string(), Some("text/markdown".to_string()), Some("# Hello".to_string()), @@ -419,8 +309,6 @@ mod tests { ); let value: serde_json::Value = serde_json::to_value(&message).unwrap(); - - // Drill into content let content = value.get("content").expect("content present"); assert_eq!( content.get("type").and_then(|v| v.as_str()), @@ -431,7 +319,6 @@ mod tests { .get("resource") .expect("resource field present at content level"); - // Spec-compliant: resource.uri / resource.mimeType / resource.text MUST be flat assert_eq!( resource.get("uri").and_then(|v| v.as_str()), Some("alc://packages/sc/narrative"), @@ -446,7 +333,6 @@ mod tests { Some("# Hello") ); - // Regression guard: content.resource MUST NOT contain a nested `resource` key. assert!( resource.get("resource").is_none(), "double-nested resource detected (regression): {resource:#?}" @@ -463,13 +349,13 @@ mod tests { "mimeType": "text/plain" }"#; - let content: PromptMessageContent = serde_json::from_str(json).unwrap(); + let content: ContentBlock = serde_json::from_str(json).unwrap(); - if let PromptMessageContent::ResourceLink { link } = content { - assert_eq!(link.uri, "file:///example.txt"); - assert_eq!(link.name, "example.txt"); - assert_eq!(link.description, Some("Example file".to_string())); - assert_eq!(link.mime_type, Some("text/plain".to_string())); + if let ContentBlock::ResourceLink(resource) = content { + assert_eq!(resource.uri, "file:///example.txt"); + assert_eq!(resource.name, "example.txt"); + assert_eq!(resource.description, Some("Example file".to_string())); + assert_eq!(resource.mime_type, Some("text/plain".to_string())); } else { panic!("Expected ResourceLink variant"); } diff --git a/crates/rmcp/src/model/resource.rs b/crates/rmcp/src/model/resource.rs index c3c7e8e81..a5ad95061 100644 --- a/crates/rmcp/src/model/resource.rs +++ b/crates/rmcp/src/model/resource.rs @@ -1,66 +1,173 @@ use serde::{Deserialize, Serialize}; -use super::{Annotated, Icon, Meta}; +use super::{Annotations, Icon, Meta}; -/// Represents a resource in the extension with metadata +/// A known resource that the server is capable of reading (spec `Resource`). +/// +/// Also used as the inner type of `ContentBlock::ResourceLink` (spec `ResourceLink extends Resource`). #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] -pub struct RawResource { - /// URI representing the resource location (e.g., "file:///path/to/file" or "str:///content") +#[non_exhaustive] +pub struct Resource { + /// The URI of this resource (e.g. `file:///path/to/file`). pub uri: String, - /// Name of the resource + /// The programmatic name of the resource. pub name: String, - /// Human-readable title of the resource + /// Optional human-readable display title. #[serde(skip_serializing_if = "Option::is_none")] pub title: Option, - /// Optional description of the resource + /// Optional description of what this resource represents. #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, - /// MIME type of the resource content ("text" or "blob") + /// The MIME type of this resource, if known. #[serde(skip_serializing_if = "Option::is_none")] pub mime_type: Option, - - /// The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known. - /// - /// This can be used by Hosts to display file sizes and estimate context window us + /// The size of the raw resource content in bytes (before base64/tokenization), if known. #[serde(skip_serializing_if = "Option::is_none")] - pub size: Option, - /// Optional list of icons for the resource + pub size: Option, + /// Optional set of icons the client may display for this resource. #[serde(skip_serializing_if = "Option::is_none")] pub icons: Option>, - /// Optional additional metadata for this resource + /// Optional protocol-level metadata for this resource. #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] pub meta: Option, + /// Optional annotations describing how the client should use this resource. + #[serde(skip_serializing_if = "Option::is_none")] + pub annotations: Option, } -pub type Resource = Annotated; +impl Resource { + pub fn new(uri: impl Into, name: impl Into) -> Self { + Self { + uri: uri.into(), + name: name.into(), + title: None, + description: None, + mime_type: None, + size: None, + icons: None, + meta: None, + annotations: None, + } + } + + pub fn with_title(mut self, title: impl Into) -> Self { + self.title = Some(title.into()); + self + } + + pub fn with_description(mut self, description: impl Into) -> Self { + self.description = Some(description.into()); + self + } + + pub fn with_mime_type(mut self, mime_type: impl Into) -> Self { + self.mime_type = Some(mime_type.into()); + self + } + + pub fn with_size(mut self, size: u64) -> Self { + self.size = Some(size); + self + } + + pub fn with_icons(mut self, icons: Vec) -> Self { + self.icons = Some(icons); + self + } + + pub fn with_meta(mut self, meta: Meta) -> Self { + self.meta = Some(meta); + self + } + + pub fn with_annotations(mut self, annotations: Annotations) -> Self { + self.annotations = Some(annotations); + self + } +} +/// A template description for resources available on the server (spec `ResourceTemplate`). #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] -pub struct RawResourceTemplate { +#[non_exhaustive] +pub struct ResourceTemplate { + /// An RFC 6570 URI template for constructing resource URIs. pub uri_template: String, + /// The programmatic name of the resource template. pub name: String, + /// Optional human-readable display title. #[serde(skip_serializing_if = "Option::is_none")] pub title: Option, + /// Optional description of what this template is for. #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, + /// The MIME type for resources matching this template, if uniform. #[serde(skip_serializing_if = "Option::is_none")] pub mime_type: Option, - /// Optional list of icons for the resource template + /// Optional set of icons the client may display for this template. #[serde(skip_serializing_if = "Option::is_none")] pub icons: Option>, + /// Optional protocol-level metadata for this resource template. + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option, + /// Optional annotations describing how the client should use this template. + #[serde(skip_serializing_if = "Option::is_none")] + pub annotations: Option, } -pub type ResourceTemplate = Annotated; +impl ResourceTemplate { + pub fn new(uri_template: impl Into, name: impl Into) -> Self { + Self { + uri_template: uri_template.into(), + name: name.into(), + title: None, + description: None, + mime_type: None, + icons: None, + meta: None, + annotations: None, + } + } + + pub fn with_title(mut self, title: impl Into) -> Self { + self.title = Some(title.into()); + self + } + + pub fn with_description(mut self, description: impl Into) -> Self { + self.description = Some(description.into()); + self + } + + pub fn with_mime_type(mut self, mime_type: impl Into) -> Self { + self.mime_type = Some(mime_type.into()); + self + } + + pub fn with_icons(mut self, icons: Vec) -> Self { + self.icons = Some(icons); + self + } + + pub fn with_meta(mut self, meta: Meta) -> Self { + self.meta = Some(meta); + self + } + + pub fn with_annotations(mut self, annotations: Annotations) -> Self { + self.annotations = Some(annotations); + self + } +} +/// The contents of a specific resource or sub-resource. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(untagged)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")] +#[non_exhaustive] pub enum ResourceContents { #[serde(rename_all = "camelCase")] TextResourceContents { @@ -83,7 +190,6 @@ pub enum ResourceContents { } impl ResourceContents { - /// Create text resource contents. pub fn text(text: impl Into, uri: impl Into) -> Self { Self::TextResourceContents { uri: uri.into(), @@ -93,7 +199,6 @@ impl ResourceContents { } } - /// Create blob resource contents. pub fn blob(blob: impl Into, uri: impl Into) -> Self { Self::BlobResourceContents { uri: uri.into(), @@ -103,7 +208,6 @@ impl ResourceContents { } } - /// Set the MIME type on this resource contents. pub fn with_mime_type(mut self, mime_type: impl Into) -> Self { match &mut self { Self::TextResourceContents { mime_type: mt, .. } => *mt = Some(mime_type.into()), @@ -112,7 +216,6 @@ impl ResourceContents { self } - /// Set the metadata on this resource contents. pub fn with_meta(mut self, meta: Meta) -> Self { match &mut self { Self::TextResourceContents { meta: m, .. } => *m = Some(meta), @@ -122,96 +225,6 @@ impl ResourceContents { } } -impl RawResource { - /// Creates a new Resource from a URI with explicit mime type - pub fn new(uri: impl Into, name: impl Into) -> Self { - Self { - uri: uri.into(), - name: name.into(), - title: None, - description: None, - mime_type: None, - size: None, - icons: None, - meta: None, - } - } - - /// Set the human-readable title. - pub fn with_title(mut self, title: impl Into) -> Self { - self.title = Some(title.into()); - self - } - - /// Set the description. - pub fn with_description(mut self, description: impl Into) -> Self { - self.description = Some(description.into()); - self - } - - /// Set the MIME type. - pub fn with_mime_type(mut self, mime_type: impl Into) -> Self { - self.mime_type = Some(mime_type.into()); - self - } - - /// Set the size in bytes. - pub fn with_size(mut self, size: u32) -> Self { - self.size = Some(size); - self - } - - /// Set the 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: Meta) -> Self { - self.meta = Some(meta); - self - } -} - -impl RawResourceTemplate { - /// Creates a new RawResourceTemplate with a URI template and name. - pub fn new(uri_template: impl Into, name: impl Into) -> Self { - Self { - uri_template: uri_template.into(), - name: name.into(), - title: None, - description: None, - mime_type: None, - icons: None, - } - } - - /// Set the human-readable title. - pub fn with_title(mut self, title: impl Into) -> Self { - self.title = Some(title.into()); - self - } - - /// Set the description. - pub fn with_description(mut self, description: impl Into) -> Self { - self.description = Some(description.into()); - self - } - - /// Set the MIME type. - pub fn with_mime_type(mut self, mime_type: impl Into) -> Self { - self.mime_type = Some(mime_type.into()); - self - } - - /// Set the icons. - pub fn with_icons(mut self, icons: Vec) -> Self { - self.icons = Some(icons); - self - } -} - #[cfg(test)] mod tests { use serde_json; @@ -221,21 +234,12 @@ mod tests { #[test] 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()), - size: Some(100), - icons: None, - meta: None, - }; + let resource = Resource::new("file:///test.txt", "test") + .with_description("Test resource") + .with_mime_type("text/plain") + .with_size(100); let json = serde_json::to_string(&resource).unwrap(); - println!("Serialized JSON: {}", json); - - // Verify it contains mimeType (camelCase) not mime_type (snake_case) assert!(json.contains("mimeType")); assert!(!json.contains("mime_type")); } @@ -250,28 +254,22 @@ mod tests { }; let json = serde_json::to_string(&text_contents).unwrap(); - println!("ResourceContents JSON: {}", json); - - // Verify it contains mimeType (camelCase) not mime_type (snake_case) assert!(json.contains("mimeType")); assert!(!json.contains("mime_type")); } #[test] fn test_resource_template_with_icons() { - let resource_template = RawResourceTemplate { - uri_template: "file:///{path}".to_string(), - name: "template".to_string(), - title: Some("Test Template".to_string()), - description: Some("A test resource template".to_string()), - mime_type: Some("text/plain".to_string()), - icons: Some(vec![Icon { + let resource_template = ResourceTemplate::new("file:///{path}", "template") + .with_title("Test Template") + .with_description("A test resource template") + .with_mime_type("text/plain") + .with_icons(vec![Icon { src: "https://example.com/icon.png".to_string(), mime_type: Some("image/png".to_string()), sizes: Some(vec!["48x48".to_string()]), theme: Some(IconTheme::Light), - }]), - }; + }]); let json = serde_json::to_value(&resource_template).unwrap(); assert!(json["icons"].is_array()); @@ -282,16 +280,31 @@ mod tests { #[test] fn test_resource_template_without_icons() { - let resource_template = RawResourceTemplate { - uri_template: "file:///{path}".to_string(), - name: "template".to_string(), - title: None, - description: None, - mime_type: None, - icons: None, - }; - + let resource_template = ResourceTemplate::new("file:///{path}", "template"); let json = serde_json::to_value(&resource_template).unwrap(); assert!(json.get("icons").is_none()); } + + #[test] + fn test_resource_size_u64() { + let resource = Resource::new("file:///big", "big").with_size(5_000_000_000); + let json = serde_json::to_value(&resource).unwrap(); + assert_eq!(json["size"], 5_000_000_000_u64); + } + + #[test] + fn test_resource_with_annotations() { + let resource = Resource::new("file:///test.txt", "test") + .with_annotations(Annotations::default().with_priority(0.9)); + let json = serde_json::to_value(&resource).unwrap(); + assert_eq!(json["annotations"]["priority"], 0.9_f32); + } + + #[test] + fn test_resource_template_with_meta() { + let resource_template = + ResourceTemplate::new("file:///{path}", "template").with_meta(Meta::default()); + let json = serde_json::to_value(&resource_template).unwrap(); + assert!(json.get("_meta").is_some()); + } } diff --git a/crates/rmcp/src/model/task.rs b/crates/rmcp/src/model/task.rs index dbdc34068..d81520c8c 100644 --- a/crates/rmcp/src/model/task.rs +++ b/crates/rmcp/src/model/task.rs @@ -3,11 +3,54 @@ use serde_json::Value; use super::Meta; +/// Metadata for augmenting a request with task execution (spec `TaskMetadata`). +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[non_exhaustive] +pub struct TaskMetadata { + #[serde(skip_serializing_if = "Option::is_none")] + pub ttl: Option, +} + +impl TaskMetadata { + pub fn new() -> Self { + Self::default() + } + + pub fn with_ttl(mut self, ttl: u64) -> Self { + self.ttl = Some(ttl); + self + } +} + +/// Metadata for associating messages with a task (spec `RelatedTaskMetadata`). +/// +/// Carried in `_meta` under the key `"io.modelcontextprotocol/related-task"`. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[non_exhaustive] +pub struct RelatedTaskMetadata { + pub task_id: String, +} + +impl RelatedTaskMetadata { + pub fn new(task_id: impl Into) -> Self { + Self { + task_id: task_id.into(), + } + } + + /// The well-known `_meta` key for related-task metadata. + pub const META_KEY: &str = "io.modelcontextprotocol/related-task"; +} + /// Canonical task lifecycle status as defined by SEP-1686. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] #[serde(rename_all = "snake_case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")] +#[non_exhaustive] pub enum TaskStatus { /// The receiver accepted the request and is currently working on it. #[default] @@ -111,7 +154,7 @@ impl CreateTaskResult { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] +#[non_exhaustive] pub struct GetTaskResult { #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, @@ -119,6 +162,12 @@ pub struct GetTaskResult { pub task: Task, } +impl GetTaskResult { + pub fn new(task: Task) -> Self { + Self { meta: None, task } + } +} + /// Response to a `tasks/result` request. /// /// Per spec, the result structure matches the original request type @@ -162,7 +211,7 @@ impl<'de> serde::Deserialize<'de> for GetTaskPayloadResult { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] +#[non_exhaustive] pub struct CancelTaskResult { #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, @@ -170,11 +219,17 @@ pub struct CancelTaskResult { pub task: Task, } +impl CancelTaskResult { + pub fn new(task: Task) -> Self { + Self { meta: None, task } + } +} + /// Paginated list of tasks #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] +#[non_exhaustive] pub struct TaskList { pub tasks: Vec, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/crates/rmcp/src/service.rs b/crates/rmcp/src/service.rs index 70045d115..29d822a58 100644 --- a/crates/rmcp/src/service.rs +++ b/crates/rmcp/src/service.rs @@ -400,8 +400,9 @@ impl RequestHandle { async fn send_timeout_cancel_notification(&self, reason: &str) { let notification = CancelledNotification { params: CancelledNotificationParam { - request_id: self.id.clone(), + request_id: Some(self.id.clone()), reason: Some(reason.to_owned()), + meta: None, }, method: crate::model::CancelledNotificationMethod, extensions: Default::default(), @@ -473,8 +474,9 @@ impl RequestHandle { .await; let notification = CancelledNotification { params: CancelledNotificationParam { - request_id: self.id, + request_id: Some(self.id), reason, + meta: None, }, method: crate::model::CancelledNotificationMethod, extensions: Default::default(), @@ -1084,11 +1086,13 @@ where }; let _ = responder.send(response); if let Some(param) = cancellation_param { - if let Some(responder) = local_responder_pool.remove(¶m.request_id) { - tracing::info!(id = %param.request_id, reason = param.reason, "cancelled"); - let _response_result = responder.send(Err(ServiceError::Cancelled { - reason: param.reason.clone(), - })); + if let Some(request_id) = ¶m.request_id { + if let Some(responder) = local_responder_pool.remove(request_id) { + tracing::info!(id = %request_id, reason = param.reason, "cancelled"); + let _response_result = responder.send(Err(ServiceError::Cancelled { + reason: param.reason.clone(), + })); + } } } } @@ -1201,9 +1205,11 @@ where // catch cancelled notification let mut notification = match notification.try_into() { Ok::(cancelled) => { - if let Some(ct) = local_ct_pool.remove(&cancelled.params.request_id) { - tracing::info!(id = %cancelled.params.request_id, reason = cancelled.params.reason, "cancelled"); - ct.cancel(); + if let Some(request_id) = &cancelled.params.request_id { + if let Some(ct) = local_ct_pool.remove(request_id) { + tracing::info!(id = %request_id, reason = cancelled.params.reason, "cancelled"); + ct.cancel(); + } } cancelled.into() } diff --git a/crates/rmcp/src/service/server.rs b/crates/rmcp/src/service/server.rs index aa51e4704..160d5b324 100644 --- a/crates/rmcp/src/service/server.rs +++ b/crates/rmcp/src/service/server.rs @@ -9,8 +9,8 @@ use url::Url; use super::*; #[cfg(feature = "elicitation")] use crate::model::{ - CreateElicitationRequest, CreateElicitationRequestParams, CreateElicitationResult, - ElicitationAction, ElicitationCompletionNotification, ElicitationResponseNotificationParam, + ElicitRequest, ElicitRequestParams, ElicitResult, ElicitationAction, + ElicitationCompleteNotification, ElicitationResponseNotificationParam, }; use crate::{ model::{ @@ -464,11 +464,11 @@ impl Peer { peer_req list_roots ListRootsRequest() => ListRootsResult ); #[cfg(feature = "elicitation")] - method!(peer_req create_elicitation CreateElicitationRequest(CreateElicitationRequestParams) => CreateElicitationResult); + method!(peer_req create_elicitation ElicitRequest(ElicitRequestParams) => ElicitResult); #[cfg(feature = "elicitation")] - method!(peer_req_with_timeout create_elicitation_with_timeout CreateElicitationRequest(CreateElicitationRequestParams) => CreateElicitationResult); + method!(peer_req_with_timeout create_elicitation_with_timeout ElicitRequest(ElicitRequestParams) => ElicitResult); #[cfg(feature = "elicitation")] - method!(peer_not notify_url_elicitation_completed ElicitationCompletionNotification(ElicitationResponseNotificationParam)); + method!(peer_not notify_url_elicitation_completed ElicitationCompleteNotification(ElicitationResponseNotificationParam)); method!(peer_not notify_cancelled CancelledNotification(CancelledNotificationParam)); method!(peer_not notify_progress ProgressNotification(ProgressNotificationParam)); @@ -787,7 +787,7 @@ impl Peer { let response = self .create_elicitation_with_timeout( - CreateElicitationRequestParams::FormElicitationParams { + ElicitRequestParams::FormElicitationParams { meta: None, message: message.into(), requested_schema: schema, @@ -920,7 +920,7 @@ impl Peer { let action = self .create_elicitation_with_timeout( - CreateElicitationRequestParams::UrlElicitationParams { + ElicitRequestParams::UrlElicitationParams { meta: None, message: message.into(), url: url.into().to_string(), diff --git a/crates/rmcp/src/transport/streamable_http_server/session/local.rs b/crates/rmcp/src/transport/streamable_http_server/session/local.rs index 54c7b558e..7e2893206 100644 --- a/crates/rmcp/src/transport/streamable_http_server/session/local.rs +++ b/crates/rmcp/src/transport/streamable_http_server/session/local.rs @@ -429,9 +429,10 @@ impl LocalSessionWorker { notification: &JsonRpcNotification, ) { if let ClientNotification::CancelledNotification(n) = ¬ification.notification { - let request_id = n.params.request_id.clone(); - let resource = ResourceKey::McpRequestId(request_id); - self.unregister_resource(&resource); + if let Some(request_id) = n.params.request_id.clone() { + let resource = ResourceKey::McpRequestId(request_id); + self.unregister_resource(&resource); + } } } fn evict_expired_channels(&mut self) { @@ -496,13 +497,17 @@ impl LocalSessionWorker { }), .. }) => { - if let Some(id) = self - .resource_router - .get(&ResourceKey::McpRequestId(request_id.clone())) - { - OutboundChannel::RequestWise { - id: *id, - close: false, + if let Some(req_id) = request_id { + if let Some(id) = self + .resource_router + .get(&ResourceKey::McpRequestId(req_id.clone())) + { + OutboundChannel::RequestWise { + id: *id, + close: false, + } + } else { + OutboundChannel::Common } } else { OutboundChannel::Common diff --git a/crates/rmcp/tests/common/handlers.rs b/crates/rmcp/tests/common/handlers.rs index dd2d16ebb..276e1bd23 100644 --- a/crates/rmcp/tests/common/handlers.rs +++ b/crates/rmcp/tests/common/handlers.rs @@ -171,10 +171,12 @@ impl ServerHandler for TestServer { }; if let Err(e) = peer - .notify_logging_message(LoggingMessageNotificationParam { - level: request.level, - data, - logger, + .notify_logging_message({ + let mut param = LoggingMessageNotificationParam::new(request.level, data); + if let Some(l) = logger { + param = param.with_logger(l); + } + param }) .await { diff --git a/crates/rmcp/tests/test_completion.rs b/crates/rmcp/tests/test_completion.rs index 694ae4d9a..9d64fae78 100644 --- a/crates/rmcp/tests/test_completion.rs +++ b/crates/rmcp/tests/test_completion.rs @@ -54,10 +54,7 @@ fn test_complete_request_param_serialization() { let request = CompleteRequestParams::new( Reference::for_prompt("weather_prompt"), - ArgumentInfo { - name: "location".to_string(), - value: "San".to_string(), - }, + ArgumentInfo::new("location", "San"), ) .with_context(CompletionContext::with_arguments(args)); @@ -144,11 +141,8 @@ fn test_reference_convenience_methods() { #[test] fn test_completion_serialization_format() { // Test that completion follows MCP 2025-06-18 specification format - let completion = CompletionInfo { - values: vec!["value1".to_string(), "value2".to_string()], - total: Some(2), - has_more: Some(false), - }; + let completion = + CompletionInfo::with_all_values(vec!["value1".to_string(), "value2".to_string()]).unwrap(); let json = serde_json::to_value(&completion).unwrap(); @@ -162,15 +156,11 @@ fn test_completion_serialization_format() { #[test] fn test_resource_reference() { - // Test that ResourceReference works correctly - let resource_ref = ResourceReference { - uri: "test://uri".to_string(), - }; + // Test that ResourceTemplateReference works correctly + let resource_ref = ResourceTemplateReference::new("test://uri"); - // Test that ResourceReference works correctly - let another_ref = ResourceReference { - uri: "test://uri".to_string(), - }; + // Test that ResourceTemplateReference works correctly + let another_ref = ResourceTemplateReference::new("test://uri"); // They should be equivalent assert_eq!(resource_ref.uri, another_ref.uri); @@ -197,10 +187,7 @@ fn test_mcp_schema_compliance() { // Test that our types serialize correctly according to MCP specification let request = CompleteRequestParams::new( Reference::for_resource("file://{path}"), - ArgumentInfo { - name: "path".to_string(), - value: "src/".to_string(), - }, + ArgumentInfo::new("path", "src/"), ); let json_str = serde_json::to_string(&request).unwrap(); diff --git a/crates/rmcp/tests/test_complex_schema.rs b/crates/rmcp/tests/test_complex_schema.rs index 0e3dc4fed..9372b0152 100644 --- a/crates/rmcp/tests/test_complex_schema.rs +++ b/crates/rmcp/tests/test_complex_schema.rs @@ -40,7 +40,7 @@ impl Demo { &self, chat_request: Parameters, ) -> Result { - let content = Content::json(chat_request.0)?; + let content = ContentBlock::json(chat_request.0)?; Ok(CallToolResult::success(vec![content])) } } diff --git a/crates/rmcp/tests/test_deserialization.rs b/crates/rmcp/tests/test_deserialization.rs index ffcb51eff..58e9a58af 100644 --- a/crates/rmcp/tests/test_deserialization.rs +++ b/crates/rmcp/tests/test_deserialization.rs @@ -126,7 +126,8 @@ mod untagged_server_result { #[test] fn round_trip_call_tool_result_preserves_variant() { - let original = CallToolResult::success(vec![rmcp::model::Content::text("hello world")]); + let original = + CallToolResult::success(vec![rmcp::model::ContentBlock::text("hello world")]); let json = serde_json::to_value(&original).unwrap(); let result = parse_result(wrap_response(json)); assert!(matches!(result, ServerResult::CallToolResult(_))); diff --git a/crates/rmcp/tests/test_elicitation.rs b/crates/rmcp/tests/test_elicitation.rs index 04a112f5b..b4a163380 100644 --- a/crates/rmcp/tests/test_elicitation.rs +++ b/crates/rmcp/tests/test_elicitation.rs @@ -36,15 +36,18 @@ async fn test_elicitation_serialization() { ); } -/// Test CreateElicitationRequestParams structure serialization/deserialization +/// Test ElicitRequestParams structure serialization/deserialization #[tokio::test] async fn test_elicitation_request_param_serialization() { let schema = ElicitationSchema::builder() - .required_property("email", PrimitiveSchema::String(StringSchema::email())) + .required_property( + "email", + PrimitiveSchemaDefinition::String(StringSchema::email()), + ) .build() .unwrap(); - let request_param = CreateElicitationRequestParams::FormElicitationParams { + let request_param = ElicitRequestParams::FormElicitationParams { meta: None, message: "Please provide your email address".to_string(), requested_schema: schema, @@ -70,15 +73,15 @@ async fn test_elicitation_request_param_serialization() { assert_eq!(json, expected); // Test deserialization - let deserialized: CreateElicitationRequestParams = serde_json::from_value(expected).unwrap(); + let deserialized: ElicitRequestParams = serde_json::from_value(expected).unwrap(); match (&deserialized, &request_param) { ( - CreateElicitationRequestParams::FormElicitationParams { + ElicitRequestParams::FormElicitationParams { meta: None, message: msg1, requested_schema: schema1, }, - CreateElicitationRequestParams::FormElicitationParams { + ElicitRequestParams::FormElicitationParams { meta: None, message: msg2, requested_schema: schema2, @@ -91,15 +94,12 @@ async fn test_elicitation_request_param_serialization() { } } -/// Test CreateElicitationResult structure with different action types +/// Test ElicitResult structure with different action types #[tokio::test] async fn test_elicitation_result_serialization() { // Test Accept with content - let accept_result = CreateElicitationResult { - action: ElicitationAction::Accept, - content: Some(json!({"email": "user@example.com"})), - meta: None, - }; + let accept_result = ElicitResult::new(ElicitationAction::Accept) + .with_content(json!({"email": "user@example.com"})); let json = serde_json::to_value(&accept_result).unwrap(); let expected = json!({ @@ -109,11 +109,7 @@ async fn test_elicitation_result_serialization() { assert_eq!(json, expected); // Test Decline without content - let decline_result = CreateElicitationResult { - action: ElicitationAction::Decline, - content: None, - meta: None, - }; + let decline_result = ElicitResult::new(ElicitationAction::Decline); let json = serde_json::to_value(&decline_result).unwrap(); let expected = json!({ @@ -123,16 +119,15 @@ async fn test_elicitation_result_serialization() { assert_eq!(json, expected); // Test deserialization - let deserialized: CreateElicitationResult = serde_json::from_value(expected).unwrap(); + let deserialized: ElicitResult = serde_json::from_value(expected).unwrap(); assert_eq!(deserialized.action, ElicitationAction::Decline); assert_eq!(deserialized.content, None); assert_eq!(deserialized.meta, None); // Test protocol-level metadata round-trips as _meta. - let meta_result = - CreateElicitationResult::new(ElicitationAction::Accept).with_meta(Meta(object!({ - "traceId": "elicitation-123" - }))); + let meta_result = ElicitResult::new(ElicitationAction::Accept).with_meta(Meta(object!({ + "traceId": "elicitation-123" + }))); let json = serde_json::to_value(&meta_result).unwrap(); let expected = json!({ @@ -141,7 +136,7 @@ async fn test_elicitation_result_serialization() { }); assert_eq!(json, expected); - let deserialized: CreateElicitationResult = serde_json::from_value(expected).unwrap(); + let deserialized: ElicitResult = serde_json::from_value(expected).unwrap(); assert_eq!( deserialized.meta, Some(Meta(object!({ "traceId": "elicitation-123" }))) @@ -154,7 +149,7 @@ async fn test_elicitation_json_rpc_protocol() { let schema = ElicitationSchema::builder() .required_property( "confirmation", - PrimitiveSchema::Boolean(BooleanSchema::new()), + PrimitiveSchemaDefinition::Boolean(BooleanSchema::new()), ) .build() .unwrap(); @@ -163,13 +158,11 @@ async fn test_elicitation_json_rpc_protocol() { let request = JsonRpcRequest { jsonrpc: JsonRpcVersion2_0, id: RequestId::Number(1), - request: CreateElicitationRequest::new( - CreateElicitationRequestParams::FormElicitationParams { - meta: None, - message: "Do you want to continue?".to_string(), - requested_schema: schema, - }, - ), + request: ElicitRequest::new(ElicitRequestParams::FormElicitationParams { + meta: None, + message: "Do you want to continue?".to_string(), + requested_schema: schema, + }), }; // Test serialization of complete request @@ -180,11 +173,10 @@ async fn test_elicitation_json_rpc_protocol() { assert_eq!(json["params"]["message"], "Do you want to continue?"); // Test deserialization - let deserialized: JsonRpcRequest = - serde_json::from_value(json).unwrap(); + let deserialized: JsonRpcRequest = serde_json::from_value(json).unwrap(); assert_eq!(deserialized.id, RequestId::Number(1)); match &deserialized.request.params { - CreateElicitationRequestParams::FormElicitationParams { message, .. } => { + ElicitRequestParams::FormElicitationParams { message, .. } => { assert_eq!(message, "Do you want to continue?"); } _ => panic!("Expected FormElicitationParam variant"), @@ -250,7 +242,7 @@ async fn test_elicitation_spec_compliance() { #[tokio::test] async fn test_elicitation_error_handling() { // Test minimal schema handling (empty properties is technically valid) - let minimal_schema_request = CreateElicitationRequestParams::FormElicitationParams { + let minimal_schema_request = ElicitRequestParams::FormElicitationParams { meta: None, message: "Test message".to_string(), requested_schema: ElicitationSchema::builder().build().unwrap(), @@ -260,11 +252,14 @@ async fn test_elicitation_error_handling() { let _json = serde_json::to_value(&minimal_schema_request).unwrap(); // Test empty message - let empty_message_request = CreateElicitationRequestParams::FormElicitationParams { + let empty_message_request = ElicitRequestParams::FormElicitationParams { meta: None, message: "".to_string(), requested_schema: ElicitationSchema::builder() - .property("value", PrimitiveSchema::String(StringSchema::new())) + .property( + "value", + PrimitiveSchemaDefinition::String(StringSchema::new()), + ) .build() .unwrap(), }; @@ -282,11 +277,14 @@ async fn test_elicitation_error_handling() { #[tokio::test] async fn test_elicitation_performance() { let schema = ElicitationSchema::builder() - .property("data", PrimitiveSchema::String(StringSchema::new())) + .property( + "data", + PrimitiveSchemaDefinition::String(StringSchema::new()), + ) .build() .unwrap(); - let request = CreateElicitationRequestParams::FormElicitationParams { + let request = ElicitRequestParams::FormElicitationParams { meta: None, message: "Performance test message".to_string(), requested_schema: schema, @@ -297,7 +295,7 @@ async fn test_elicitation_performance() { // Serialize/deserialize 1000 times for _ in 0..1000 { let json = serde_json::to_value(&request).unwrap(); - let _deserialized: CreateElicitationRequestParams = serde_json::from_value(json).unwrap(); + let _deserialized: ElicitRequestParams = serde_json::from_value(json).unwrap(); } let duration = start.elapsed(); @@ -326,9 +324,7 @@ async fn test_elicitation_capabilities() { assert_eq!(elicitation_cap.url, None); // Test with schema validation enabled - elicitation_cap.form = Some(FormElicitationCapability { - schema_validation: Some(true), - }); + elicitation_cap.form = Some(FormElicitationCapability::new().with_schema_validation(true)); // Test serialization let json = serde_json::to_value(&elicitation_cap).unwrap(); @@ -423,14 +419,14 @@ async fn test_elicitation_convenience_methods() { .contains("Option A") ); - // Test that CreateElicitationRequestParam can be created with type-safe schemas - let confirmation_request = CreateElicitationRequestParams::FormElicitationParams { + // Test that ElicitRequestParams can be created with type-safe schemas + let confirmation_request = ElicitRequestParams::FormElicitationParams { meta: None, message: "Test confirmation".to_string(), requested_schema: ElicitationSchema::builder() .property( "confirmed", - PrimitiveSchema::Boolean( + PrimitiveSchemaDefinition::Boolean( BooleanSchema::new() .description("User confirmation (true for yes, false for no)"), ), @@ -467,7 +463,7 @@ async fn test_elicitation_structured_schemas() { .build() .unwrap(); - let request = CreateElicitationRequestParams::FormElicitationParams { + let request = ElicitRequestParams::FormElicitationParams { meta: None, message: "Please provide your user information".to_string(), requested_schema: schema, @@ -475,10 +471,10 @@ async fn test_elicitation_structured_schemas() { // Test that complex schemas serialize/deserialize correctly let json = serde_json::to_value(&request).unwrap(); - let deserialized: CreateElicitationRequestParams = serde_json::from_value(json).unwrap(); + let deserialized: ElicitRequestParams = serde_json::from_value(json).unwrap(); match deserialized { - CreateElicitationRequestParams::FormElicitationParams { + ElicitRequestParams::FormElicitationParams { message, requested_schema, .. @@ -699,7 +695,7 @@ async fn test_elicitation_multi_select_enum() { .build() .unwrap(); - let request = CreateElicitationRequestParams::FormElicitationParams { + let request = ElicitRequestParams::FormElicitationParams { meta: None, message: "Please provide your user information".to_string(), requested_schema: schema, @@ -707,10 +703,10 @@ async fn test_elicitation_multi_select_enum() { // Test that complex schemas serialize/deserialize correctly let json = serde_json::to_value(&request).unwrap(); - let deserialized: CreateElicitationRequestParams = serde_json::from_value(json).unwrap(); + let deserialized: ElicitRequestParams = serde_json::from_value(json).unwrap(); match deserialized { - CreateElicitationRequestParams::FormElicitationParams { + ElicitRequestParams::FormElicitationParams { message, requested_schema, .. @@ -722,30 +718,20 @@ async fn test_elicitation_multi_select_enum() { assert!(matches!( requested_schema.properties.get("choices").unwrap(), - PrimitiveSchema::Enum(EnumSchema::Multi(_)) + PrimitiveSchemaDefinition::Enum(EnumSchema::Multi(_)) )); - if let Some(PrimitiveSchema::Enum(schema)) = requested_schema.properties.get("choices") + if let Some(PrimitiveSchemaDefinition::Enum(schema)) = + requested_schema.properties.get("choices") { assert_eq!( schema, &EnumSchema::Multi(MultiSelectEnumSchema::Titled( - TitledMultiSelectEnumSchema::new(TitledItems { - any_of: vec![ - ConstTitle { - const_: "A".to_string(), - title: "A name".to_string() - }, - ConstTitle { - const_: "B".to_string(), - title: "B name".to_string() - }, - ConstTitle { - const_: "C".to_string(), - title: "C name".to_string() - }, - ], - }) + TitledMultiSelectEnumSchema::new(TitledItems::new(vec![ + ConstTitle::new("A", "A name"), + ConstTitle::new("B", "B name"), + ConstTitle::new("C", "C name"), + ])) .with_min_items(1) .with_max_items(2) )) @@ -773,7 +759,7 @@ async fn test_elicitation_single_select_enum() { .build() .unwrap(); - let request = CreateElicitationRequestParams::FormElicitationParams { + let request = ElicitRequestParams::FormElicitationParams { meta: None, message: "Please provide your user information".to_string(), requested_schema: schema, @@ -781,10 +767,10 @@ async fn test_elicitation_single_select_enum() { // Test that complex schemas serialize/deserialize correctly let json = serde_json::to_value(&request).unwrap(); - let deserialized: CreateElicitationRequestParams = serde_json::from_value(json).unwrap(); + let deserialized: ElicitRequestParams = serde_json::from_value(json).unwrap(); match deserialized { - CreateElicitationRequestParams::FormElicitationParams { + ElicitRequestParams::FormElicitationParams { message, requested_schema, .. @@ -795,27 +781,19 @@ async fn test_elicitation_single_select_enum() { assert_eq!(requested_schema.required, Some(vec!["choices".to_string()])); assert!(matches!( requested_schema.properties.get("choices").unwrap(), - PrimitiveSchema::Enum(EnumSchema::Single(_)) + PrimitiveSchemaDefinition::Enum(EnumSchema::Single(_)) )); - if let Some(PrimitiveSchema::Enum(schema)) = requested_schema.properties.get("choices") + if let Some(PrimitiveSchemaDefinition::Enum(schema)) = + requested_schema.properties.get("choices") { assert_eq!( schema, &EnumSchema::Single(SingleSelectEnumSchema::Titled( TitledSingleSelectEnumSchema::new(vec![ - ConstTitle { - const_: "A".to_string(), - title: "A name".to_string() - }, - ConstTitle { - const_: "B".to_string(), - title: "B name".to_string() - }, - ConstTitle { - const_: "C".to_string(), - title: "C name".to_string() - } + ConstTitle::new("A", "A name"), + ConstTitle::new("B", "B name"), + ConstTitle::new("C", "C name"), ]) )) ) @@ -841,12 +819,12 @@ async fn test_elicitation_direction_server_to_client() { let schema = ElicitationSchema::builder() .property( "name", - PrimitiveSchema::String(StringSchema::new().description("Enter your name")), + PrimitiveSchemaDefinition::String(StringSchema::new().description("Enter your name")), ) .build() .unwrap(); - let elicitation_request = CreateElicitationRequestParams::FormElicitationParams { + let elicitation_request = ElicitRequestParams::FormElicitationParams { meta: None, message: "Please enter your name".to_string(), requested_schema: schema, @@ -858,23 +836,20 @@ async fn test_elicitation_direction_server_to_client() { assert_eq!(serialized["requestedSchema"]["type"], "object"); // Test that elicitation requests are part of ServerRequest - let _server_request = - ServerRequest::CreateElicitationRequest(CreateElicitationRequest::new(elicitation_request)); + let _server_request = ServerRequest::ElicitRequest(ElicitRequest::new(elicitation_request)); // Test that client can respond with elicitation results - let client_result = ClientResult::CreateElicitationResult(CreateElicitationResult { - action: ElicitationAction::Accept, - content: Some(json!("John Doe")), - meta: None, - }); + let client_result = ClientResult::ElicitResult( + ElicitResult::new(ElicitationAction::Accept).with_content(json!("John Doe")), + ); // Verify client result can be serialized match client_result { - ClientResult::CreateElicitationResult(result) => { + ClientResult::ElicitResult(result) => { assert_eq!(result.action, ElicitationAction::Accept); assert_eq!(result.content, Some(json!("John Doe"))); } - _ => panic!("CreateElicitationResult should be part of ClientResult"), + _ => panic!("ElicitResult should be part of ClientResult"), } } @@ -888,15 +863,17 @@ async fn test_elicitation_json_rpc_direction() { let schema = ElicitationSchema::builder() .property( "continue", - PrimitiveSchema::Boolean(BooleanSchema::new().description("Do you want to continue?")), + PrimitiveSchemaDefinition::Boolean( + BooleanSchema::new().description("Do you want to continue?"), + ), ) .build() .unwrap(); // 1. Server creates elicitation request let server_request = ServerJsonRpcMessage::request( - ServerRequest::CreateElicitationRequest(CreateElicitationRequest::new( - CreateElicitationRequestParams::FormElicitationParams { + ServerRequest::ElicitRequest(ElicitRequest::new( + ElicitRequestParams::FormElicitationParams { meta: None, message: "Do you want to continue?".to_string(), requested_schema: schema, @@ -913,11 +890,9 @@ async fn test_elicitation_json_rpc_direction() { // 2. Client responds with elicitation result let client_response = ClientJsonRpcMessage::response( - ClientResult::CreateElicitationResult(CreateElicitationResult { - action: ElicitationAction::Accept, - content: Some(json!(true)), - meta: None, - }), + ClientResult::ElicitResult( + ElicitResult::new(ElicitationAction::Accept).with_content(json!(true)), + ), RequestId::Number(1), ); @@ -946,13 +921,12 @@ async fn test_elicitation_actions_compliance() { ]; for action in actions { - let result = CreateElicitationResult { - action: action.clone(), - content: match action { - ElicitationAction::Accept => Some(serde_json::json!("some data")), - _ => None, - }, - meta: None, + let result = { + let r = ElicitResult::new(action.clone()); + match action { + ElicitationAction::Accept => r.with_content(serde_json::json!("some data")), + _ => r, + } }; let json = serde_json::to_value(&result).unwrap(); @@ -970,28 +944,25 @@ async fn test_elicitation_actions_compliance() { assert_eq!(json["action"], "cancel"); assert!(json.get("content").is_none() || json["content"].is_null()); } + _ => {} } } } -/// Test that CreateElicitationResult IS in ClientResult (response compliance) +/// Test that ElicitResult IS in ClientResult (response compliance) #[tokio::test] async fn test_elicitation_result_in_client_result() { use rmcp::model::*; // Test that clients can return elicitation results - let result = ClientResult::CreateElicitationResult(CreateElicitationResult { - action: ElicitationAction::Decline, - content: None, - meta: None, - }); + let result = ClientResult::ElicitResult(ElicitResult::new(ElicitationAction::Decline)); match result { - ClientResult::CreateElicitationResult(elicit_result) => { + ClientResult::ElicitResult(elicit_result) => { assert_eq!(elicit_result.action, ElicitationAction::Decline); assert_eq!(elicit_result.content, None); } - _ => panic!("CreateElicitationResult should be part of ClientResult"), + _ => panic!("ElicitResult should be part of ClientResult"), } } @@ -1008,24 +979,16 @@ async fn test_elicitation_capability_structure() { assert!(default_cap.url.is_none()); // Test ElicitationCapability with schema validation enabled - let cap_with_validation = ElicitationCapability { - form: Some(FormElicitationCapability { - schema_validation: Some(true), - }), - url: None, - }; + let cap_with_validation = ElicitationCapability::new() + .with_form(FormElicitationCapability::new().with_schema_validation(true)); assert_eq!( cap_with_validation.form.as_ref().unwrap().schema_validation, Some(true) ); // Test ElicitationCapability with schema validation disabled - let cap_without_validation = ElicitationCapability { - form: Some(FormElicitationCapability { - schema_validation: Some(false), - }), - url: None, - }; + let cap_without_validation = ElicitationCapability::new() + .with_form(FormElicitationCapability::new().with_schema_validation(false)); assert_eq!( cap_without_validation .form @@ -1059,12 +1022,10 @@ async fn test_elicitation_capability_structure() { async fn test_client_capabilities_with_elicitation() { // Test ClientCapabilities with elicitation capability let capabilities = ClientCapabilities::builder() - .enable_elicitation_with(ElicitationCapability { - form: Some(FormElicitationCapability { - schema_validation: Some(true), - }), - url: None, - }) + .enable_elicitation_with( + ElicitationCapability::new() + .with_form(FormElicitationCapability::new().with_schema_validation(true)), + ) .build(); // Verify elicitation capability is present @@ -1101,12 +1062,10 @@ async fn test_initialize_request_with_elicitation() { // Test InitializeRequestParam with elicitation capability let init_param = InitializeRequestParams::new( ClientCapabilities::builder() - .enable_elicitation_with(ElicitationCapability { - form: Some(FormElicitationCapability { - schema_validation: Some(true), - }), - url: None, - }) + .enable_elicitation_with( + ElicitationCapability::new() + .with_form(FormElicitationCapability::new().with_schema_validation(true)), + ) .build(), Implementation::new("test-client", "1.0.0"), ); @@ -1143,12 +1102,10 @@ async fn test_capability_checking_logic() { // Case 1: Client with elicitation capability let client_with_capability = InitializeRequestParams::new( ClientCapabilities::builder() - .enable_elicitation_with(ElicitationCapability { - form: Some(FormElicitationCapability { - schema_validation: Some(true), - }), - url: None, - }) + .enable_elicitation_with( + ElicitationCapability::new() + .with_form(FormElicitationCapability::new().with_schema_validation(true)), + ) .build(), Implementation::new("test-client", "1.0.0"), ); @@ -1257,12 +1214,8 @@ async fn test_elicitation_capability_serialization() { assert_eq!(json, serde_json::json!({})); // Test capability with schema validation enabled - let cap_with_validation = ElicitationCapability { - form: Some(FormElicitationCapability { - schema_validation: Some(true), - }), - url: None, - }; + let cap_with_validation = ElicitationCapability::new() + .with_form(FormElicitationCapability::new().with_schema_validation(true)); let json = serde_json::to_value(&cap_with_validation).unwrap(); assert_eq!( @@ -1275,12 +1228,8 @@ async fn test_elicitation_capability_serialization() { ); // Test capability with schema validation disabled - let cap_without_validation = ElicitationCapability { - form: Some(FormElicitationCapability { - schema_validation: Some(false), - }), - url: None, - }; + let cap_without_validation = ElicitationCapability::new() + .with_form(FormElicitationCapability::new().with_schema_validation(false)); let json = serde_json::to_value(&cap_without_validation).unwrap(); assert_eq!( @@ -1332,12 +1281,8 @@ async fn test_client_capabilities_elicitation_builder() { ); // Test enabling elicitation with custom capability - let custom_elicitation = ElicitationCapability { - form: Some(FormElicitationCapability { - schema_validation: Some(false), - }), - url: None, - }; + let custom_elicitation = ElicitationCapability::new() + .with_form(FormElicitationCapability::new().with_schema_validation(false)); let caps_custom = ClientCapabilities::builder() .enable_elicitation_with(custom_elicitation.clone()) @@ -1361,12 +1306,18 @@ async fn test_create_elicitation_with_timeout_basic() { // This test verifies that the method accepts timeout parameter let schema = ElicitationSchema::builder() - .required_property("name", PrimitiveSchema::String(StringSchema::new())) - .required_property("email", PrimitiveSchema::String(StringSchema::new())) + .required_property( + "name", + PrimitiveSchemaDefinition::String(StringSchema::new()), + ) + .required_property( + "email", + PrimitiveSchemaDefinition::String(StringSchema::new()), + ) .build() .unwrap(); - let _params = CreateElicitationRequestParams::FormElicitationParams { + let _params = ElicitRequestParams::FormElicitationParams { meta: None, message: "Enter your details".to_string(), requested_schema: schema, @@ -1547,6 +1498,7 @@ async fn test_elicitation_action_error_mapping() { let error = ElicitationError::UserCancelled; assert!(format!("{}", error).contains("cancelled/dismissed")); } + _ => {} } } } @@ -1690,7 +1642,10 @@ async fn test_elicitation_examples_compile() { async fn test_build_validation_required_field_not_in_properties() { // Try to mark a field as required that doesn't exist in properties let result = ElicitationSchema::builder() - .property("email", PrimitiveSchema::String(StringSchema::email())) + .property( + "email", + PrimitiveSchemaDefinition::String(StringSchema::email()), + ) .mark_required("nonexistent_field") .build(); @@ -1706,8 +1661,14 @@ async fn test_build_validation_required_field_not_in_properties() { #[tokio::test] async fn test_build_validation_required_field_exists() { let result = ElicitationSchema::builder() - .property("email", PrimitiveSchema::String(StringSchema::email())) - .property("name", PrimitiveSchema::String(StringSchema::new())) + .property( + "email", + PrimitiveSchemaDefinition::String(StringSchema::email()), + ) + .property( + "name", + PrimitiveSchemaDefinition::String(StringSchema::new()), + ) .mark_required("email") .mark_required("name") .build(); @@ -1728,7 +1689,10 @@ async fn test_build_validation_required_field_exists() { async fn test_build_unchecked_panics_on_invalid() { // build_unchecked validates but panics instead of returning Result let _schema = ElicitationSchema::builder() - .property("email", PrimitiveSchema::String(StringSchema::email())) + .property( + "email", + PrimitiveSchemaDefinition::String(StringSchema::email()), + ) .mark_required("nonexistent_field") .build_unchecked(); } @@ -1776,25 +1740,25 @@ async fn test_typed_property_methods() { assert_eq!(schema.properties.len(), 4); // Verify types are correct - if let Some(PrimitiveSchema::String(_)) = schema.properties.get("name") { + if let Some(PrimitiveSchemaDefinition::String(_)) = schema.properties.get("name") { // Expected } else { panic!("name should be StringSchema"); } - if let Some(PrimitiveSchema::Number(_)) = schema.properties.get("price") { + if let Some(PrimitiveSchemaDefinition::Number(_)) = schema.properties.get("price") { // Expected } else { panic!("price should be NumberSchema"); } - if let Some(PrimitiveSchema::Integer(_)) = schema.properties.get("quantity") { + if let Some(PrimitiveSchemaDefinition::Integer(_)) = schema.properties.get("quantity") { // Expected } else { panic!("quantity should be IntegerSchema"); } - if let Some(PrimitiveSchema::Boolean(_)) = schema.properties.get("in_stock") { + if let Some(PrimitiveSchemaDefinition::Boolean(_)) = schema.properties.get("in_stock") { // Expected } else { panic!("in_stock should be BooleanSchema"); @@ -1831,7 +1795,7 @@ async fn test_required_typed_property_methods() { /// Test URL elicitation request parameter serialization/deserialization #[tokio::test] async fn test_url_elicitation_request_param_serialization() { - let request_param = CreateElicitationRequestParams::UrlElicitationParams { + let request_param = ElicitRequestParams::UrlElicitationParams { meta: None, message: "Please visit the following URL to complete verification".to_string(), url: "https://example.com/verify".to_string(), @@ -1850,9 +1814,9 @@ async fn test_url_elicitation_request_param_serialization() { assert_eq!(json, expected); // Test deserialization - let deserialized: CreateElicitationRequestParams = serde_json::from_value(expected).unwrap(); + let deserialized: ElicitRequestParams = serde_json::from_value(expected).unwrap(); match deserialized { - CreateElicitationRequestParams::UrlElicitationParams { + ElicitRequestParams::UrlElicitationParams { message, url, elicitation_id, @@ -1876,14 +1840,12 @@ async fn test_url_elicitation_json_rpc_protocol() { let request = JsonRpcRequest { jsonrpc: JsonRpcVersion2_0, id: RequestId::Number(1), - request: CreateElicitationRequest::new( - CreateElicitationRequestParams::UrlElicitationParams { - meta: None, - message: "Please authorize this action at the following URL".to_string(), - url: "https://auth.example.com/authorize/abc123".to_string(), - elicitation_id: "auth-request-456".to_string(), - }, - ), + request: ElicitRequest::new(ElicitRequestParams::UrlElicitationParams { + meta: None, + message: "Please authorize this action at the following URL".to_string(), + url: "https://auth.example.com/authorize/abc123".to_string(), + elicitation_id: "auth-request-456".to_string(), + }), }; // Test serialization of complete request @@ -1903,11 +1865,10 @@ async fn test_url_elicitation_json_rpc_protocol() { assert_eq!(json["params"]["elicitationId"], "auth-request-456"); // Test deserialization - let deserialized: JsonRpcRequest = - serde_json::from_value(json).unwrap(); + let deserialized: JsonRpcRequest = serde_json::from_value(json).unwrap(); assert_eq!(deserialized.id, RequestId::Number(1)); match &deserialized.request.params { - CreateElicitationRequestParams::UrlElicitationParams { + ElicitRequestParams::UrlElicitationParams { message, url, elicitation_id, @@ -1921,12 +1882,10 @@ async fn test_url_elicitation_json_rpc_protocol() { } } -/// Test ElicitationCompletionNotification serialization/deserialization +/// Test ElicitationCompleteNotification serialization/deserialization #[tokio::test] async fn test_elicitation_completion_notification() { - let notification_params = ElicitationResponseNotificationParam { - elicitation_id: "elicit-789".to_string(), - }; + let notification_params = ElicitationResponseNotificationParam::new("elicit-789"); // Test serialization let json = serde_json::to_value(¬ification_params).unwrap(); @@ -1941,7 +1900,7 @@ async fn test_elicitation_completion_notification() { assert_eq!(deserialized.elicitation_id, "elicit-789"); // Test complete notification structure - let notification = ElicitationCompletionNotification::new(notification_params); + let notification = ElicitationCompleteNotification::new(notification_params); let json = serde_json::to_value(¬ification).unwrap(); assert_eq!(json["method"], "notifications/elicitation/complete"); @@ -1963,10 +1922,7 @@ async fn test_url_elicitation_capability() { assert_eq!(deserialized, url_cap); // Test ElicitationCapability with URL mode enabled - let elicitation_cap = ElicitationCapability { - form: None, - url: Some(UrlElicitationCapability::default()), - }; + let elicitation_cap = ElicitationCapability::new().with_url(UrlElicitationCapability::new()); let json = serde_json::to_value(&elicitation_cap).unwrap(); assert_eq!( @@ -1977,12 +1933,9 @@ async fn test_url_elicitation_capability() { ); // Test ElicitationCapability with both form and URL modes - let both_cap = ElicitationCapability { - form: Some(FormElicitationCapability { - schema_validation: Some(true), - }), - url: Some(UrlElicitationCapability::default()), - }; + let both_cap = ElicitationCapability::new() + .with_form(FormElicitationCapability::new().with_schema_validation(true)) + .with_url(UrlElicitationCapability::new()); let json = serde_json::to_value(&both_cap).unwrap(); assert_eq!( @@ -1996,7 +1949,7 @@ async fn test_url_elicitation_capability() { ); } -/// Test backward compatibility: CreateElicitationRequestParam without mode tag +/// Test backward compatibility: ElicitRequestParams without mode tag #[tokio::test] async fn test_elicitation_backward_compatibility_no_mode() { // JSON without "mode" field should deserialize as FormElicitationParam @@ -2013,11 +1966,10 @@ async fn test_elicitation_backward_compatibility_no_mode() { } }); - let deserialized: CreateElicitationRequestParams = - serde_json::from_value(json_without_mode).unwrap(); + let deserialized: ElicitRequestParams = serde_json::from_value(json_without_mode).unwrap(); match deserialized { - CreateElicitationRequestParams::FormElicitationParams { + ElicitRequestParams::FormElicitationParams { message, requested_schema, .. @@ -2035,11 +1987,14 @@ async fn test_elicitation_backward_compatibility_no_mode() { async fn test_elicitation_both_modes() { // Form mode let form_schema = ElicitationSchema::builder() - .required_property("email", PrimitiveSchema::String(StringSchema::email())) + .required_property( + "email", + PrimitiveSchemaDefinition::String(StringSchema::email()), + ) .build() .unwrap(); - let form_request = CreateElicitationRequestParams::FormElicitationParams { + let form_request = ElicitRequestParams::FormElicitationParams { meta: None, message: "Enter email".to_string(), requested_schema: form_schema, @@ -2051,7 +2006,7 @@ async fn test_elicitation_both_modes() { assert!(form_json.get("url").is_none()); // URL mode - let url_request = CreateElicitationRequestParams::UrlElicitationParams { + let url_request = ElicitRequestParams::UrlElicitationParams { meta: None, message: "Visit URL".to_string(), url: "https://example.com".to_string(), @@ -2103,12 +2058,10 @@ async fn test_url_elicitation_required_error_code() { async fn test_client_capabilities_elicitation_modes() { // Test with form-only capability let form_only_caps = ClientCapabilities::builder() - .enable_elicitation_with(ElicitationCapability { - form: Some(FormElicitationCapability { - schema_validation: Some(true), - }), - url: None, - }) + .enable_elicitation_with( + ElicitationCapability::new() + .with_form(FormElicitationCapability::new().with_schema_validation(true)), + ) .build(); let json = serde_json::to_value(&form_only_caps).unwrap(); @@ -2120,10 +2073,9 @@ async fn test_client_capabilities_elicitation_modes() { // Test with URL-only capability let url_only_caps = ClientCapabilities::builder() - .enable_elicitation_with(ElicitationCapability { - form: None, - url: Some(UrlElicitationCapability::default()), - }) + .enable_elicitation_with( + ElicitationCapability::new().with_url(UrlElicitationCapability::new()), + ) .build(); let json = serde_json::to_value(&url_only_caps).unwrap(); @@ -2138,12 +2090,11 @@ async fn test_client_capabilities_elicitation_modes() { // Test with both capabilities let both_caps = ClientCapabilities::builder() - .enable_elicitation_with(ElicitationCapability { - form: Some(FormElicitationCapability { - schema_validation: Some(false), - }), - url: Some(UrlElicitationCapability::default()), - }) + .enable_elicitation_with( + ElicitationCapability::new() + .with_form(FormElicitationCapability::new().with_schema_validation(false)) + .with_url(UrlElicitationCapability::new()), + ) .build(); let json = serde_json::to_value(&both_caps).unwrap(); @@ -2151,19 +2102,16 @@ async fn test_client_capabilities_elicitation_modes() { assert!(json["elicitation"]["url"].is_object()); } -/// Test ElicitationCompletionNotification in ServerNotification enum +/// Test ElicitationCompleteNotification in ServerNotification enum #[tokio::test] async fn test_elicitation_completion_in_server_notification() { - let notification_param = ElicitationResponseNotificationParam { - elicitation_id: "notify-123".to_string(), - }; + let notification_param = ElicitationResponseNotificationParam::new("notify-123"); - let completion_notification = - ElicitationCompletionNotification::new(notification_param.clone()); + let completion_notification = ElicitationCompleteNotification::new(notification_param.clone()); // Test that it's part of ServerNotification let server_notification = - ServerNotification::ElicitationCompletionNotification(completion_notification); + ServerNotification::ElicitationCompleteNotification(completion_notification); // Test serialization let json = serde_json::to_value(&server_notification).unwrap(); @@ -2173,10 +2121,10 @@ async fn test_elicitation_completion_in_server_notification() { // Test deserialization let deserialized: ServerNotification = serde_json::from_value(json).unwrap(); match deserialized { - ServerNotification::ElicitationCompletionNotification(notif) => { + ServerNotification::ElicitationCompleteNotification(notif) => { assert_eq!(notif.params.elicitation_id, "notify-123"); } - _ => panic!("Expected ElicitationCompletionNotification variant"), + _ => panic!("Expected ElicitationCompleteNotification variant"), } } @@ -2184,11 +2132,7 @@ async fn test_elicitation_completion_in_server_notification() { #[tokio::test] async fn test_url_elicitation_action_workflow() { // Test Accept action for URL elicitation (user visited URL and confirmed) - let accept_result = CreateElicitationResult { - action: ElicitationAction::Accept, - content: None, // URL elicitation doesn't return content, just confirmation - meta: None, - }; + let accept_result = ElicitResult::new(ElicitationAction::Accept); let json = serde_json::to_value(&accept_result).unwrap(); assert_eq!(json["action"], "accept"); @@ -2196,21 +2140,13 @@ async fn test_url_elicitation_action_workflow() { assert!(json.get("content").is_none() || json["content"].is_null()); // Test Decline action for URL elicitation - let decline_result = CreateElicitationResult { - action: ElicitationAction::Decline, - content: None, - meta: None, - }; + let decline_result = ElicitResult::new(ElicitationAction::Decline); let json = serde_json::to_value(&decline_result).unwrap(); assert_eq!(json["action"], "decline"); // Test Cancel action for URL elicitation - let cancel_result = CreateElicitationResult { - action: ElicitationAction::Cancel, - content: None, - meta: None, - }; + let cancel_result = ElicitResult::new(ElicitationAction::Cancel); let json = serde_json::to_value(&cancel_result).unwrap(); assert_eq!(json["action"], "cancel"); diff --git a/crates/rmcp/tests/test_embedded_resource_meta.rs b/crates/rmcp/tests/test_embedded_resource_meta.rs index 7535e358f..167108e8e 100644 --- a/crates/rmcp/tests/test_embedded_resource_meta.rs +++ b/crates/rmcp/tests/test_embedded_resource_meta.rs @@ -1,26 +1,23 @@ -use rmcp::model::{AnnotateAble, Content, Meta, RawContent, ResourceContents}; +use rmcp::model::{ContentBlock, EmbeddedResource, Meta, ResourceContents}; use serde_json::json; #[test] fn serialize_embedded_text_resource_with_meta() { - // Inner contents meta let mut resource_content_meta = Meta::new(); resource_content_meta.insert("inner".to_string(), json!(2)); - // Top-level embedded resource meta let mut resource_meta = Meta::new(); resource_meta.insert("top".to_string(), json!(1)); - let content: Content = RawContent::Resource(rmcp::model::RawEmbeddedResource { - meta: Some(resource_meta), - resource: ResourceContents::TextResourceContents { + let content = ContentBlock::Resource( + EmbeddedResource::new(ResourceContents::TextResourceContents { uri: "str://example".to_string(), mime_type: Some("text/plain".to_string()), text: "hello".to_string(), meta: Some(resource_content_meta), - }, - }) - .no_annotation(); + }) + .with_meta(resource_meta), + ); let v = serde_json::to_value(&content).unwrap(); @@ -40,16 +37,14 @@ fn serialize_embedded_text_resource_with_meta() { #[test] fn serialize_embedded_text_resource_without_meta_omits_fields() { - let content: Content = RawContent::Resource(rmcp::model::RawEmbeddedResource { - meta: None, - resource: ResourceContents::TextResourceContents { + let content = ContentBlock::Resource(EmbeddedResource::new( + ResourceContents::TextResourceContents { uri: "str://no-meta".to_string(), mime_type: Some("text/plain".to_string()), text: "hi".to_string(), meta: None, }, - }) - .no_annotation(); + )); let v = serde_json::to_value(&content).unwrap(); @@ -70,19 +65,17 @@ fn deserialize_embedded_text_resource_with_meta() { } }); - let content: Content = serde_json::from_value(raw).unwrap(); + let content: ContentBlock = serde_json::from_value(raw).unwrap(); - let raw = match &content.raw { - RawContent::Resource(er) => er, + let er = match &content { + ContentBlock::Resource(er) => er, _ => panic!("expected resource"), }; - // top-level _meta - let top = raw.meta.as_ref().expect("top-level meta missing"); + let top = er.meta.as_ref().expect("top-level meta missing"); assert_eq!(top.get("x").unwrap(), &json!(true)); - // inner contents _meta - match &raw.resource { + match &er.resource { ResourceContents::TextResourceContents { meta, uri, text, .. } => { @@ -103,16 +96,15 @@ fn serialize_embedded_blob_resource_with_meta() { let mut resource_meta = Meta::new(); resource_meta.insert("blob_top".to_string(), json!("t")); - let content: Content = RawContent::Resource(rmcp::model::RawEmbeddedResource { - meta: Some(resource_meta), - resource: ResourceContents::BlobResourceContents { + let content = ContentBlock::Resource( + EmbeddedResource::new(ResourceContents::BlobResourceContents { uri: "str://blob".to_string(), mime_type: Some("application/octet-stream".to_string()), blob: "Zm9v".to_string(), meta: Some(resource_content_meta), - }, - }) - .no_annotation(); + }) + .with_meta(resource_meta), + ); let v = serde_json::to_value(&content).unwrap(); diff --git a/crates/rmcp/tests/test_inflight_response_drain.rs b/crates/rmcp/tests/test_inflight_response_drain.rs index 2381644d9..8af62ba53 100644 --- a/crates/rmcp/tests/test_inflight_response_drain.rs +++ b/crates/rmcp/tests/test_inflight_response_drain.rs @@ -148,7 +148,7 @@ async fn test_inflight_response_drain_on_eof() -> anyhow::Result<()> { let text = result .content .first() - .and_then(|c| c.raw.as_text()) + .and_then(|c| c.as_text()) .map(|t| t.text.as_str()) .expect("expected text content in tool result"); assert_eq!(text, "done after 200ms"); diff --git a/crates/rmcp/tests/test_logging.rs b/crates/rmcp/tests/test_logging.rs index 467cf7134..bf00352bc 100644 --- a/crates/rmcp/tests/test_logging.rs +++ b/crates/rmcp/tests/test_logging.rs @@ -26,14 +26,16 @@ async fn test_logging_spec_compliance() -> anyhow::Result<()> { // Test server can send messages before level is set server .peer() - .notify_logging_message(LoggingMessageNotificationParam { - level: LoggingLevel::Info, - data: serde_json::json!({ - "message": "Server initiated message", - "timestamp": chrono::Utc::now().to_rfc3339(), - }), - logger: Some("test_server".to_string()), - }) + .notify_logging_message( + LoggingMessageNotificationParam::new( + LoggingLevel::Info, + serde_json::json!({ + "message": "Server initiated message", + "timestamp": chrono::Utc::now().to_rfc3339(), + }), + ) + .with_logger("test_server"), + ) .await?; server.waiting().await?; @@ -277,14 +279,9 @@ async fn test_logging_optional_fields() -> anyhow::Result<()> { // Test message with and without optional logger field for (level, has_logger) in [(LoggingLevel::Info, true), (LoggingLevel::Debug, false)] { - server - .peer() - .notify_logging_message(LoggingMessageNotificationParam { - level, - data: json!({"test": "data"}), - logger: has_logger.then(|| "test_logger".to_string()), - }) - .await?; + let mut param = LoggingMessageNotificationParam::new(level, json!({"test": "data"})); + param.logger = has_logger.then(|| "test_logger".to_string()); + server.peer().notify_logging_message(param).await?; } server.waiting().await?; 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 952aff8e4..6a6da4cec 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 @@ -37,109 +37,8 @@ } ], "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/RawAudioContent" - } - ], - "required": [ - "type" - ] - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "resource_link" - } - }, - "allOf": [ - { - "$ref": "#/definitions/RawResource" - } - ], - "required": [ - "type" - ] - } - ] - }, "Annotations": { + "description": "Optional annotations for the client. The client can use annotations to inform how objects are\nused or displayed.", "type": "object", "properties": { "audience": { @@ -182,6 +81,43 @@ "value" ] }, + "AudioContent": { + "description": "Audio content with base64-encoded data (spec `AudioContent`).", + "type": "object", + "properties": { + "_meta": { + "description": "Optional protocol-level metadata for this content block.", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "annotations": { + "description": "Optional annotations describing how the client should use this content.", + "anyOf": [ + { + "$ref": "#/definitions/Annotations" + }, + { + "type": "null" + } + ] + }, + "data": { + "description": "The base64-encoded audio data.", + "type": "string" + }, + "mimeType": { + "description": "The MIME type of the audio (e.g. `audio/wav`).", + "type": "string" + } + }, + "required": [ + "data", + "mimeType" + ] + }, "CallToolRequestMethod": { "type": "string", "format": "const", @@ -213,11 +149,14 @@ }, "task": { "description": "Task metadata for async task management (SEP-1319)", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/TaskMetadata" + }, + { + "type": "null" + } + ] } }, "required": [ @@ -256,6 +195,13 @@ "CancelledNotificationParam": { "type": "object", "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "reason": { "type": [ "string", @@ -263,12 +209,16 @@ ] }, "requestId": { - "$ref": "#/definitions/NumberOrString" + "anyOf": [ + { + "$ref": "#/definitions/NumberOrString" + }, + { + "type": "null" + } + ] } - }, - "required": [ - "requestId" - ] + } }, "ClientCapabilities": { "title": "Builder", @@ -350,7 +300,7 @@ "$ref": "#/definitions/ListRootsResult" }, { - "$ref": "#/definitions/CreateElicitationResult" + "$ref": "#/definitions/ElicitResult" }, { "$ref": "#/definitions/EmptyObject" @@ -415,32 +365,94 @@ } } }, - "CreateElicitationResult": { - "description": "The result returned by a client in response to an elicitation request.\n\nContains the user's decision (accept/decline/cancel) and optionally their input data\nif they chose to accept the request.", - "type": "object", - "properties": { - "_meta": { - "description": "Optional protocol-level metadata for this result.", - "type": [ - "object", - "null" + "ContentBlock": { + "description": "Unified content block union (spec `ContentBlock`).\n\n`text | image | audio | resource_link | resource`", + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "text" + } + }, + "allOf": [ + { + "$ref": "#/definitions/TextContent" + } ], - "additionalProperties": true + "required": [ + "type" + ] }, - "action": { - "description": "The user's decision on how to handle the elicitation request", + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "image" + } + }, "allOf": [ { - "$ref": "#/definitions/ElicitationAction" + "$ref": "#/definitions/ImageContent" } + ], + "required": [ + "type" ] }, - "content": { - "description": "The actual data provided by the user, if they accepted the request.\nMust conform to the JSON schema specified in the original request.\nOnly present when action is Accept." + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "audio" + } + }, + "allOf": [ + { + "$ref": "#/definitions/AudioContent" + } + ], + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "resource" + } + }, + "allOf": [ + { + "$ref": "#/definitions/EmbeddedResource" + } + ], + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "resource_link" + } + }, + "allOf": [ + { + "$ref": "#/definitions/Resource" + } + ], + "required": [ + "type" + ] } - }, - "required": [ - "action" ] }, "CreateMessageResult": { @@ -517,6 +529,34 @@ "CustomResult": { "description": "A catch-all response either side can use for custom requests." }, + "ElicitResult": { + "description": "The result returned by a client in response to an elicitation request.\n\nContains the user's decision (accept/decline/cancel) and optionally their input data\nif they chose to accept the request.", + "type": "object", + "properties": { + "_meta": { + "description": "Optional protocol-level metadata for this result.", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "action": { + "description": "The user's decision on how to handle the elicitation request", + "allOf": [ + { + "$ref": "#/definitions/ElicitationAction" + } + ] + }, + "content": { + "description": "The actual data provided by the user, if they accepted the request.\nMust conform to the JSON schema specified in the original request.\nOnly present when action is Accept." + } + }, + "required": [ + "action" + ] + }, "ElicitationAction": { "description": "Represents the possible actions a user can take in response to an elicitation request.\n\nWhen a server requests user input through elicitation, the user can:\n- Accept: Provide the requested information and continue\n- Decline: Refuse to provide the information but continue the operation\n- Cancel: Stop the entire operation", "oneOf": [ @@ -577,6 +617,42 @@ } } }, + "EmbeddedResource": { + "description": "Embedded resource content (spec `EmbeddedResource`).", + "type": "object", + "properties": { + "_meta": { + "description": "Optional protocol-level metadata for this content block.", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "annotations": { + "description": "Optional annotations describing how the client should use this content.", + "anyOf": [ + { + "$ref": "#/definitions/Annotations" + }, + { + "type": "null" + } + ] + }, + "resource": { + "description": "The embedded resource contents (text or blob).", + "allOf": [ + { + "$ref": "#/definitions/ResourceContents" + } + ] + } + }, + "required": [ + "resource" + ] + }, "EmptyObject": { "description": "This is commonly used for representing empty objects in MCP messages.\n\nwithout returning any specific data.", "type": "object", @@ -657,12 +733,12 @@ "name" ] }, - "GetTaskInfoMethod": { + "GetTaskMethod": { "type": "string", "format": "const", "const": "tasks/get" }, - "GetTaskInfoParams": { + "GetTaskParams": { "type": "object", "properties": { "_meta": { @@ -681,12 +757,12 @@ "taskId" ] }, - "GetTaskResultMethod": { + "GetTaskPayloadMethod": { "type": "string", "format": "const", "const": "tasks/result" }, - "GetTaskResultParams": { + "GetTaskPayloadParams": { "type": "object", "properties": { "_meta": { @@ -761,6 +837,43 @@ } ] }, + "ImageContent": { + "description": "Image content with base64-encoded data (spec `ImageContent`).", + "type": "object", + "properties": { + "_meta": { + "description": "Optional protocol-level metadata for this content block.", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "annotations": { + "description": "Optional annotations describing how the client should use this content.", + "anyOf": [ + { + "$ref": "#/definitions/Annotations" + }, + { + "type": "null" + } + ] + }, + "data": { + "description": "The base64-encoded image data.", + "type": "string" + }, + "mimeType": { + "description": "The MIME type of the image (e.g. `image/png`).", + "type": "string" + } + }, + "required": [ + "data", + "mimeType" + ] + }, "Implementation": { "type": "object", "properties": { @@ -901,6 +1014,9 @@ { "$ref": "#/definitions/NotificationNoParam2" }, + { + "$ref": "#/definitions/Notification3" + }, { "$ref": "#/definitions/CustomNotification" } @@ -1087,271 +1203,157 @@ "params" ] }, - "NotificationNoParam": { + "Notification3": { "type": "object", "properties": { "method": { - "$ref": "#/definitions/InitializedNotificationMethod" + "$ref": "#/definitions/TaskStatusNotificationMethod" + }, + "params": { + "$ref": "#/definitions/Task" } }, "required": [ - "method" + "method", + "params" ] }, - "NotificationNoParam2": { + "NotificationNoParam": { "type": "object", "properties": { "method": { - "$ref": "#/definitions/RootsListChangedNotificationMethod" + "$ref": "#/definitions/InitializedNotificationMethod" } }, "required": [ "method" ] }, - "NumberOrString": { - "oneOf": [ - { - "type": "number" - }, - { - "type": "string" - } - ] - }, - "PaginatedRequestParams": { - "type": "object", - "properties": { - "_meta": { - "description": "Protocol-level metadata for this request (SEP-1319)", - "type": [ - "object", - "null" - ], - "additionalProperties": true - }, - "cursor": { - "type": [ - "string", - "null" - ] - } - } - }, - "PingRequestMethod": { - "type": "string", - "format": "const", - "const": "ping" - }, - "ProgressNotificationMethod": { - "type": "string", - "format": "const", - "const": "notifications/progress" - }, - "ProgressNotificationParam": { + "NotificationNoParam2": { "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": "number", - "format": "double" - }, - "progressToken": { - "$ref": "#/definitions/ProgressToken" - }, - "total": { - "description": "Total number of items to process (or total progress required), if known", - "type": [ - "number", - "null" - ], - "format": "double" + "method": { + "$ref": "#/definitions/RootsListChangedNotificationMethod" } }, "required": [ - "progressToken", - "progress" + "method" ] }, - "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": [ + "NumberOrString": { + "oneOf": [ { - "$ref": "#/definitions/NumberOrString" - } - ] - }, - "PromptReference": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "title": { - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "name" - ] - }, - "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" - }, - "RawAudioContent": { - "type": "object", - "properties": { - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType" - ] - }, - "RawEmbeddedResource": { - "type": "object", - "properties": { - "_meta": { - "description": "Optional protocol-level metadata for this content block", - "type": [ - "object", - "null" - ], - "additionalProperties": true - }, - "resource": { - "$ref": "#/definitions/ResourceContents" - } - }, - "required": [ - "resource" - ] - }, - "RawImageContent": { - "type": "object", - "properties": { - "_meta": { - "description": "Optional protocol-level metadata for this content block", - "type": [ - "object", - "null" - ], - "additionalProperties": true - }, - "data": { - "description": "The base64-encoded image", - "type": "string" + "type": "number" }, - "mimeType": { + { "type": "string" } - }, - "required": [ - "data", - "mimeType" ] }, - "RawResource": { - "description": "Represents a resource in the extension with metadata", + "PaginatedRequestParams": { "type": "object", "properties": { "_meta": { - "description": "Optional additional metadata for this resource", + "description": "Protocol-level metadata for this request (SEP-1319)", "type": [ "object", "null" ], "additionalProperties": true }, - "description": { - "description": "Optional description of the resource", + "cursor": { "type": [ "string", "null" ] - }, - "icons": { - "description": "Optional list of icons for the resource", + } + } + }, + "PingRequestMethod": { + "type": "string", + "format": "const", + "const": "ping" + }, + "ProgressNotificationMethod": { + "type": "string", + "format": "const", + "const": "notifications/progress" + }, + "ProgressNotificationParam": { + "type": "object", + "properties": { + "_meta": { "type": [ - "array", + "object", "null" ], - "items": { - "$ref": "#/definitions/Icon" - } + "additionalProperties": true }, - "mimeType": { - "description": "MIME type of the resource content (\"text\" or \"blob\")", + "message": { + "description": "An optional message describing the current progress.", "type": [ "string", "null" ] }, - "name": { - "description": "Name of the resource", - "type": "string" + "progress": { + "description": "The progress thus far. This should increase every time progress is made, even if the total is unknown.", + "type": "number", + "format": "double" }, - "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 + "progressToken": { + "$ref": "#/definitions/ProgressToken" }, - "title": { - "description": "Human-readable title of the resource", + "total": { + "description": "Total number of items to process (or total progress required), if known", "type": [ - "string", + "number", "null" - ] - }, - "uri": { - "description": "URI representing the resource location (e.g., \"file:///path/to/file\" or \"str:///content\")", - "type": "string" + ], + "format": "double" } }, "required": [ - "uri", - "name" + "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" + } ] }, - "RawTextContent": { + "PromptReference": { "type": "object", "properties": { "_meta": { - "description": "Optional protocol-level metadata for this content block", "type": [ "object", "null" ], "additionalProperties": true }, - "text": { + "name": { "type": "string" + }, + "title": { + "type": [ + "string", + "null" + ] } }, "required": [ - "text" + "name" ] }, + "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" + }, "ReadResourceRequestMethod": { "type": "string", "format": "const", @@ -1390,7 +1392,7 @@ }, "allOf": [ { - "$ref": "#/definitions/ResourceReference" + "$ref": "#/definitions/ResourceTemplateReference" } ], "required": [ @@ -1437,10 +1439,10 @@ "type": "object", "properties": { "method": { - "$ref": "#/definitions/GetTaskResultMethod" + "$ref": "#/definitions/GetTaskPayloadMethod" }, "params": { - "$ref": "#/definitions/GetTaskResultParams" + "$ref": "#/definitions/GetTaskPayloadParams" } }, "required": [ @@ -1581,10 +1583,10 @@ "type": "object", "properties": { "method": { - "$ref": "#/definitions/GetTaskInfoMethod" + "$ref": "#/definitions/GetTaskMethod" }, "params": { - "$ref": "#/definitions/GetTaskInfoParams" + "$ref": "#/definitions/GetTaskParams" } }, "required": [ @@ -1708,7 +1710,85 @@ "method" ] }, + "Resource": { + "description": "A known resource that the server is capable of reading (spec `Resource`).\n\nAlso used as the inner type of `ContentBlock::ResourceLink` (spec `ResourceLink extends Resource`).", + "type": "object", + "properties": { + "_meta": { + "description": "Optional protocol-level metadata for this resource.", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "annotations": { + "description": "Optional annotations describing how the client should use this resource.", + "anyOf": [ + { + "$ref": "#/definitions/Annotations" + }, + { + "type": "null" + } + ] + }, + "description": { + "description": "Optional description of what this resource represents.", + "type": [ + "string", + "null" + ] + }, + "icons": { + "description": "Optional set of icons the client may display for this resource.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Icon" + } + }, + "mimeType": { + "description": "The MIME type of this resource, if known.", + "type": [ + "string", + "null" + ] + }, + "name": { + "description": "The programmatic name of the resource.", + "type": "string" + }, + "size": { + "description": "The size of the raw resource content in bytes (before base64/tokenization), if known.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "title": { + "description": "Optional human-readable display title.", + "type": [ + "string", + "null" + ] + }, + "uri": { + "description": "The URI of this resource (e.g. `file:///path/to/file`).", + "type": "string" + } + }, + "required": [ + "uri", + "name" + ] + }, "ResourceContents": { + "description": "The contents of a specific resource or sub-resource.", "anyOf": [ { "type": "object", @@ -1768,7 +1848,7 @@ } ] }, - "ResourceReference": { + "ResourceTemplateReference": { "type": "object", "properties": { "uri": { @@ -1797,6 +1877,13 @@ "Root": { "type": "object", "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "name": { "type": [ "string", @@ -1854,17 +1941,17 @@ "description": "Single or array content wrapper (SEP-1577).", "anyOf": [ { - "$ref": "#/definitions/SamplingMessageContent" + "$ref": "#/definitions/SamplingMessageContentBlock" }, { "type": "array", "items": { - "$ref": "#/definitions/SamplingMessageContent" + "$ref": "#/definitions/SamplingMessageContentBlock" } } ] }, - "SamplingMessageContent": { + "SamplingMessageContentBlock": { "description": "Content types for sampling messages (SEP-1577).", "oneOf": [ { @@ -1877,7 +1964,7 @@ }, "allOf": [ { - "$ref": "#/definitions/RawTextContent" + "$ref": "#/definitions/TextContent" } ], "required": [ @@ -1894,7 +1981,7 @@ }, "allOf": [ { - "$ref": "#/definitions/RawImageContent" + "$ref": "#/definitions/ImageContent" } ], "required": [ @@ -1911,7 +1998,7 @@ }, "allOf": [ { - "$ref": "#/definitions/RawAudioContent" + "$ref": "#/definitions/AudioContent" } ], "required": [ @@ -2025,6 +2112,77 @@ "uri" ] }, + "Task": { + "description": "Primary Task object that surfaces metadata during the task lifecycle.\n\nPer spec, `lastUpdatedAt` and `ttl` are required fields.\n`ttl` is nullable (`null` means unlimited retention).", + "type": "object", + "properties": { + "createdAt": { + "description": "ISO-8601 creation timestamp.", + "type": "string" + }, + "lastUpdatedAt": { + "description": "ISO-8601 timestamp for the most recent status change.", + "type": "string" + }, + "pollInterval": { + "description": "Suggested polling interval (milliseconds).", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "status": { + "description": "Current lifecycle status (see [`TaskStatus`]).", + "allOf": [ + { + "$ref": "#/definitions/TaskStatus" + } + ] + }, + "statusMessage": { + "description": "Optional human-readable status message for UI surfaces.", + "type": [ + "string", + "null" + ] + }, + "taskId": { + "description": "Unique task identifier generated by the receiver.", + "type": "string" + }, + "ttl": { + "description": "Retention window in milliseconds that the receiver agreed to honor.\n`None` (serialized as `null`) means unlimited retention.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "taskId", + "status", + "createdAt", + "lastUpdatedAt" + ] + }, + "TaskMetadata": { + "description": "Metadata for augmenting a request with task execution (spec `TaskMetadata`).", + "type": "object", + "properties": { + "ttl": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + } + } + }, "TaskRequestsCapability": { "description": "Request types that support task-augmented execution.", "type": "object", @@ -2061,6 +2219,41 @@ } } }, + "TaskStatus": { + "description": "Canonical task lifecycle status as defined by SEP-1686.", + "oneOf": [ + { + "description": "The receiver accepted the request and is currently working on it.", + "type": "string", + "const": "working" + }, + { + "description": "The receiver requires additional input before work can continue.", + "type": "string", + "const": "input_required" + }, + { + "description": "The underlying operation completed successfully and the result is ready.", + "type": "string", + "const": "completed" + }, + { + "description": "The underlying operation failed and will not continue.", + "type": "string", + "const": "failed" + }, + { + "description": "The task was cancelled and will not continue processing.", + "type": "string", + "const": "cancelled" + } + ] + }, + "TaskStatusNotificationMethod": { + "type": "string", + "format": "const", + "const": "notifications/tasks/status" + }, "TasksCapability": { "description": "Task capabilities shared by client and server.", "type": "object", @@ -2091,12 +2284,43 @@ } } }, + "TextContent": { + "description": "Text content block (spec `TextContent`).", + "type": "object", + "properties": { + "_meta": { + "description": "Optional protocol-level metadata for this content block.", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "annotations": { + "description": "Optional annotations describing how the client should use this content.", + "anyOf": [ + { + "$ref": "#/definitions/Annotations" + }, + { + "type": "null" + } + ] + }, + "text": { + "description": "The text content of the message.", + "type": "string" + } + }, + "required": [ + "text" + ] + }, "ToolResultContent": { "description": "Tool execution result in user message (SEP-1577).", "type": "object", "properties": { "_meta": { - "description": "Optional metadata", "type": [ "object", "null" @@ -2104,21 +2328,18 @@ "additionalProperties": true }, "content": { - "description": "Content blocks returned by the tool", "type": "array", "items": { - "$ref": "#/definitions/Annotated" + "$ref": "#/definitions/ContentBlock" } }, "isError": { - "description": "Whether tool execution failed", "type": [ "boolean", "null" ] }, "structuredContent": { - "description": "Optional structured result", "type": [ "object", "null" @@ -2126,7 +2347,6 @@ "additionalProperties": true }, "toolUseId": { - "description": "ID of the corresponding tool use", "type": "string" } }, @@ -2139,7 +2359,6 @@ "type": "object", "properties": { "_meta": { - "description": "Optional metadata (preserved for caching)", "type": [ "object", "null" @@ -2147,16 +2366,13 @@ "additionalProperties": true }, "id": { - "description": "Unique identifier for this tool call", "type": "string" }, "input": { - "description": "Input arguments for the tool", "type": "object", "additionalProperties": true }, "name": { - "description": "Name of the tool to call", "type": "string" } }, 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 952aff8e4..6a6da4cec 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 @@ -37,109 +37,8 @@ } ], "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/RawAudioContent" - } - ], - "required": [ - "type" - ] - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "resource_link" - } - }, - "allOf": [ - { - "$ref": "#/definitions/RawResource" - } - ], - "required": [ - "type" - ] - } - ] - }, "Annotations": { + "description": "Optional annotations for the client. The client can use annotations to inform how objects are\nused or displayed.", "type": "object", "properties": { "audience": { @@ -182,6 +81,43 @@ "value" ] }, + "AudioContent": { + "description": "Audio content with base64-encoded data (spec `AudioContent`).", + "type": "object", + "properties": { + "_meta": { + "description": "Optional protocol-level metadata for this content block.", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "annotations": { + "description": "Optional annotations describing how the client should use this content.", + "anyOf": [ + { + "$ref": "#/definitions/Annotations" + }, + { + "type": "null" + } + ] + }, + "data": { + "description": "The base64-encoded audio data.", + "type": "string" + }, + "mimeType": { + "description": "The MIME type of the audio (e.g. `audio/wav`).", + "type": "string" + } + }, + "required": [ + "data", + "mimeType" + ] + }, "CallToolRequestMethod": { "type": "string", "format": "const", @@ -213,11 +149,14 @@ }, "task": { "description": "Task metadata for async task management (SEP-1319)", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/TaskMetadata" + }, + { + "type": "null" + } + ] } }, "required": [ @@ -256,6 +195,13 @@ "CancelledNotificationParam": { "type": "object", "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "reason": { "type": [ "string", @@ -263,12 +209,16 @@ ] }, "requestId": { - "$ref": "#/definitions/NumberOrString" + "anyOf": [ + { + "$ref": "#/definitions/NumberOrString" + }, + { + "type": "null" + } + ] } - }, - "required": [ - "requestId" - ] + } }, "ClientCapabilities": { "title": "Builder", @@ -350,7 +300,7 @@ "$ref": "#/definitions/ListRootsResult" }, { - "$ref": "#/definitions/CreateElicitationResult" + "$ref": "#/definitions/ElicitResult" }, { "$ref": "#/definitions/EmptyObject" @@ -415,32 +365,94 @@ } } }, - "CreateElicitationResult": { - "description": "The result returned by a client in response to an elicitation request.\n\nContains the user's decision (accept/decline/cancel) and optionally their input data\nif they chose to accept the request.", - "type": "object", - "properties": { - "_meta": { - "description": "Optional protocol-level metadata for this result.", - "type": [ - "object", - "null" + "ContentBlock": { + "description": "Unified content block union (spec `ContentBlock`).\n\n`text | image | audio | resource_link | resource`", + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "text" + } + }, + "allOf": [ + { + "$ref": "#/definitions/TextContent" + } ], - "additionalProperties": true + "required": [ + "type" + ] }, - "action": { - "description": "The user's decision on how to handle the elicitation request", + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "image" + } + }, "allOf": [ { - "$ref": "#/definitions/ElicitationAction" + "$ref": "#/definitions/ImageContent" } + ], + "required": [ + "type" ] }, - "content": { - "description": "The actual data provided by the user, if they accepted the request.\nMust conform to the JSON schema specified in the original request.\nOnly present when action is Accept." + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "audio" + } + }, + "allOf": [ + { + "$ref": "#/definitions/AudioContent" + } + ], + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "resource" + } + }, + "allOf": [ + { + "$ref": "#/definitions/EmbeddedResource" + } + ], + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "resource_link" + } + }, + "allOf": [ + { + "$ref": "#/definitions/Resource" + } + ], + "required": [ + "type" + ] } - }, - "required": [ - "action" ] }, "CreateMessageResult": { @@ -517,6 +529,34 @@ "CustomResult": { "description": "A catch-all response either side can use for custom requests." }, + "ElicitResult": { + "description": "The result returned by a client in response to an elicitation request.\n\nContains the user's decision (accept/decline/cancel) and optionally their input data\nif they chose to accept the request.", + "type": "object", + "properties": { + "_meta": { + "description": "Optional protocol-level metadata for this result.", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "action": { + "description": "The user's decision on how to handle the elicitation request", + "allOf": [ + { + "$ref": "#/definitions/ElicitationAction" + } + ] + }, + "content": { + "description": "The actual data provided by the user, if they accepted the request.\nMust conform to the JSON schema specified in the original request.\nOnly present when action is Accept." + } + }, + "required": [ + "action" + ] + }, "ElicitationAction": { "description": "Represents the possible actions a user can take in response to an elicitation request.\n\nWhen a server requests user input through elicitation, the user can:\n- Accept: Provide the requested information and continue\n- Decline: Refuse to provide the information but continue the operation\n- Cancel: Stop the entire operation", "oneOf": [ @@ -577,6 +617,42 @@ } } }, + "EmbeddedResource": { + "description": "Embedded resource content (spec `EmbeddedResource`).", + "type": "object", + "properties": { + "_meta": { + "description": "Optional protocol-level metadata for this content block.", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "annotations": { + "description": "Optional annotations describing how the client should use this content.", + "anyOf": [ + { + "$ref": "#/definitions/Annotations" + }, + { + "type": "null" + } + ] + }, + "resource": { + "description": "The embedded resource contents (text or blob).", + "allOf": [ + { + "$ref": "#/definitions/ResourceContents" + } + ] + } + }, + "required": [ + "resource" + ] + }, "EmptyObject": { "description": "This is commonly used for representing empty objects in MCP messages.\n\nwithout returning any specific data.", "type": "object", @@ -657,12 +733,12 @@ "name" ] }, - "GetTaskInfoMethod": { + "GetTaskMethod": { "type": "string", "format": "const", "const": "tasks/get" }, - "GetTaskInfoParams": { + "GetTaskParams": { "type": "object", "properties": { "_meta": { @@ -681,12 +757,12 @@ "taskId" ] }, - "GetTaskResultMethod": { + "GetTaskPayloadMethod": { "type": "string", "format": "const", "const": "tasks/result" }, - "GetTaskResultParams": { + "GetTaskPayloadParams": { "type": "object", "properties": { "_meta": { @@ -761,6 +837,43 @@ } ] }, + "ImageContent": { + "description": "Image content with base64-encoded data (spec `ImageContent`).", + "type": "object", + "properties": { + "_meta": { + "description": "Optional protocol-level metadata for this content block.", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "annotations": { + "description": "Optional annotations describing how the client should use this content.", + "anyOf": [ + { + "$ref": "#/definitions/Annotations" + }, + { + "type": "null" + } + ] + }, + "data": { + "description": "The base64-encoded image data.", + "type": "string" + }, + "mimeType": { + "description": "The MIME type of the image (e.g. `image/png`).", + "type": "string" + } + }, + "required": [ + "data", + "mimeType" + ] + }, "Implementation": { "type": "object", "properties": { @@ -901,6 +1014,9 @@ { "$ref": "#/definitions/NotificationNoParam2" }, + { + "$ref": "#/definitions/Notification3" + }, { "$ref": "#/definitions/CustomNotification" } @@ -1087,271 +1203,157 @@ "params" ] }, - "NotificationNoParam": { + "Notification3": { "type": "object", "properties": { "method": { - "$ref": "#/definitions/InitializedNotificationMethod" + "$ref": "#/definitions/TaskStatusNotificationMethod" + }, + "params": { + "$ref": "#/definitions/Task" } }, "required": [ - "method" + "method", + "params" ] }, - "NotificationNoParam2": { + "NotificationNoParam": { "type": "object", "properties": { "method": { - "$ref": "#/definitions/RootsListChangedNotificationMethod" + "$ref": "#/definitions/InitializedNotificationMethod" } }, "required": [ "method" ] }, - "NumberOrString": { - "oneOf": [ - { - "type": "number" - }, - { - "type": "string" - } - ] - }, - "PaginatedRequestParams": { - "type": "object", - "properties": { - "_meta": { - "description": "Protocol-level metadata for this request (SEP-1319)", - "type": [ - "object", - "null" - ], - "additionalProperties": true - }, - "cursor": { - "type": [ - "string", - "null" - ] - } - } - }, - "PingRequestMethod": { - "type": "string", - "format": "const", - "const": "ping" - }, - "ProgressNotificationMethod": { - "type": "string", - "format": "const", - "const": "notifications/progress" - }, - "ProgressNotificationParam": { + "NotificationNoParam2": { "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": "number", - "format": "double" - }, - "progressToken": { - "$ref": "#/definitions/ProgressToken" - }, - "total": { - "description": "Total number of items to process (or total progress required), if known", - "type": [ - "number", - "null" - ], - "format": "double" + "method": { + "$ref": "#/definitions/RootsListChangedNotificationMethod" } }, "required": [ - "progressToken", - "progress" + "method" ] }, - "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": [ + "NumberOrString": { + "oneOf": [ { - "$ref": "#/definitions/NumberOrString" - } - ] - }, - "PromptReference": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "title": { - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "name" - ] - }, - "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" - }, - "RawAudioContent": { - "type": "object", - "properties": { - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType" - ] - }, - "RawEmbeddedResource": { - "type": "object", - "properties": { - "_meta": { - "description": "Optional protocol-level metadata for this content block", - "type": [ - "object", - "null" - ], - "additionalProperties": true - }, - "resource": { - "$ref": "#/definitions/ResourceContents" - } - }, - "required": [ - "resource" - ] - }, - "RawImageContent": { - "type": "object", - "properties": { - "_meta": { - "description": "Optional protocol-level metadata for this content block", - "type": [ - "object", - "null" - ], - "additionalProperties": true - }, - "data": { - "description": "The base64-encoded image", - "type": "string" + "type": "number" }, - "mimeType": { + { "type": "string" } - }, - "required": [ - "data", - "mimeType" ] }, - "RawResource": { - "description": "Represents a resource in the extension with metadata", + "PaginatedRequestParams": { "type": "object", "properties": { "_meta": { - "description": "Optional additional metadata for this resource", + "description": "Protocol-level metadata for this request (SEP-1319)", "type": [ "object", "null" ], "additionalProperties": true }, - "description": { - "description": "Optional description of the resource", + "cursor": { "type": [ "string", "null" ] - }, - "icons": { - "description": "Optional list of icons for the resource", + } + } + }, + "PingRequestMethod": { + "type": "string", + "format": "const", + "const": "ping" + }, + "ProgressNotificationMethod": { + "type": "string", + "format": "const", + "const": "notifications/progress" + }, + "ProgressNotificationParam": { + "type": "object", + "properties": { + "_meta": { "type": [ - "array", + "object", "null" ], - "items": { - "$ref": "#/definitions/Icon" - } + "additionalProperties": true }, - "mimeType": { - "description": "MIME type of the resource content (\"text\" or \"blob\")", + "message": { + "description": "An optional message describing the current progress.", "type": [ "string", "null" ] }, - "name": { - "description": "Name of the resource", - "type": "string" + "progress": { + "description": "The progress thus far. This should increase every time progress is made, even if the total is unknown.", + "type": "number", + "format": "double" }, - "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 + "progressToken": { + "$ref": "#/definitions/ProgressToken" }, - "title": { - "description": "Human-readable title of the resource", + "total": { + "description": "Total number of items to process (or total progress required), if known", "type": [ - "string", + "number", "null" - ] - }, - "uri": { - "description": "URI representing the resource location (e.g., \"file:///path/to/file\" or \"str:///content\")", - "type": "string" + ], + "format": "double" } }, "required": [ - "uri", - "name" + "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" + } ] }, - "RawTextContent": { + "PromptReference": { "type": "object", "properties": { "_meta": { - "description": "Optional protocol-level metadata for this content block", "type": [ "object", "null" ], "additionalProperties": true }, - "text": { + "name": { "type": "string" + }, + "title": { + "type": [ + "string", + "null" + ] } }, "required": [ - "text" + "name" ] }, + "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" + }, "ReadResourceRequestMethod": { "type": "string", "format": "const", @@ -1390,7 +1392,7 @@ }, "allOf": [ { - "$ref": "#/definitions/ResourceReference" + "$ref": "#/definitions/ResourceTemplateReference" } ], "required": [ @@ -1437,10 +1439,10 @@ "type": "object", "properties": { "method": { - "$ref": "#/definitions/GetTaskResultMethod" + "$ref": "#/definitions/GetTaskPayloadMethod" }, "params": { - "$ref": "#/definitions/GetTaskResultParams" + "$ref": "#/definitions/GetTaskPayloadParams" } }, "required": [ @@ -1581,10 +1583,10 @@ "type": "object", "properties": { "method": { - "$ref": "#/definitions/GetTaskInfoMethod" + "$ref": "#/definitions/GetTaskMethod" }, "params": { - "$ref": "#/definitions/GetTaskInfoParams" + "$ref": "#/definitions/GetTaskParams" } }, "required": [ @@ -1708,7 +1710,85 @@ "method" ] }, + "Resource": { + "description": "A known resource that the server is capable of reading (spec `Resource`).\n\nAlso used as the inner type of `ContentBlock::ResourceLink` (spec `ResourceLink extends Resource`).", + "type": "object", + "properties": { + "_meta": { + "description": "Optional protocol-level metadata for this resource.", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "annotations": { + "description": "Optional annotations describing how the client should use this resource.", + "anyOf": [ + { + "$ref": "#/definitions/Annotations" + }, + { + "type": "null" + } + ] + }, + "description": { + "description": "Optional description of what this resource represents.", + "type": [ + "string", + "null" + ] + }, + "icons": { + "description": "Optional set of icons the client may display for this resource.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Icon" + } + }, + "mimeType": { + "description": "The MIME type of this resource, if known.", + "type": [ + "string", + "null" + ] + }, + "name": { + "description": "The programmatic name of the resource.", + "type": "string" + }, + "size": { + "description": "The size of the raw resource content in bytes (before base64/tokenization), if known.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "title": { + "description": "Optional human-readable display title.", + "type": [ + "string", + "null" + ] + }, + "uri": { + "description": "The URI of this resource (e.g. `file:///path/to/file`).", + "type": "string" + } + }, + "required": [ + "uri", + "name" + ] + }, "ResourceContents": { + "description": "The contents of a specific resource or sub-resource.", "anyOf": [ { "type": "object", @@ -1768,7 +1848,7 @@ } ] }, - "ResourceReference": { + "ResourceTemplateReference": { "type": "object", "properties": { "uri": { @@ -1797,6 +1877,13 @@ "Root": { "type": "object", "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "name": { "type": [ "string", @@ -1854,17 +1941,17 @@ "description": "Single or array content wrapper (SEP-1577).", "anyOf": [ { - "$ref": "#/definitions/SamplingMessageContent" + "$ref": "#/definitions/SamplingMessageContentBlock" }, { "type": "array", "items": { - "$ref": "#/definitions/SamplingMessageContent" + "$ref": "#/definitions/SamplingMessageContentBlock" } } ] }, - "SamplingMessageContent": { + "SamplingMessageContentBlock": { "description": "Content types for sampling messages (SEP-1577).", "oneOf": [ { @@ -1877,7 +1964,7 @@ }, "allOf": [ { - "$ref": "#/definitions/RawTextContent" + "$ref": "#/definitions/TextContent" } ], "required": [ @@ -1894,7 +1981,7 @@ }, "allOf": [ { - "$ref": "#/definitions/RawImageContent" + "$ref": "#/definitions/ImageContent" } ], "required": [ @@ -1911,7 +1998,7 @@ }, "allOf": [ { - "$ref": "#/definitions/RawAudioContent" + "$ref": "#/definitions/AudioContent" } ], "required": [ @@ -2025,6 +2112,77 @@ "uri" ] }, + "Task": { + "description": "Primary Task object that surfaces metadata during the task lifecycle.\n\nPer spec, `lastUpdatedAt` and `ttl` are required fields.\n`ttl` is nullable (`null` means unlimited retention).", + "type": "object", + "properties": { + "createdAt": { + "description": "ISO-8601 creation timestamp.", + "type": "string" + }, + "lastUpdatedAt": { + "description": "ISO-8601 timestamp for the most recent status change.", + "type": "string" + }, + "pollInterval": { + "description": "Suggested polling interval (milliseconds).", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "status": { + "description": "Current lifecycle status (see [`TaskStatus`]).", + "allOf": [ + { + "$ref": "#/definitions/TaskStatus" + } + ] + }, + "statusMessage": { + "description": "Optional human-readable status message for UI surfaces.", + "type": [ + "string", + "null" + ] + }, + "taskId": { + "description": "Unique task identifier generated by the receiver.", + "type": "string" + }, + "ttl": { + "description": "Retention window in milliseconds that the receiver agreed to honor.\n`None` (serialized as `null`) means unlimited retention.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "taskId", + "status", + "createdAt", + "lastUpdatedAt" + ] + }, + "TaskMetadata": { + "description": "Metadata for augmenting a request with task execution (spec `TaskMetadata`).", + "type": "object", + "properties": { + "ttl": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + } + } + }, "TaskRequestsCapability": { "description": "Request types that support task-augmented execution.", "type": "object", @@ -2061,6 +2219,41 @@ } } }, + "TaskStatus": { + "description": "Canonical task lifecycle status as defined by SEP-1686.", + "oneOf": [ + { + "description": "The receiver accepted the request and is currently working on it.", + "type": "string", + "const": "working" + }, + { + "description": "The receiver requires additional input before work can continue.", + "type": "string", + "const": "input_required" + }, + { + "description": "The underlying operation completed successfully and the result is ready.", + "type": "string", + "const": "completed" + }, + { + "description": "The underlying operation failed and will not continue.", + "type": "string", + "const": "failed" + }, + { + "description": "The task was cancelled and will not continue processing.", + "type": "string", + "const": "cancelled" + } + ] + }, + "TaskStatusNotificationMethod": { + "type": "string", + "format": "const", + "const": "notifications/tasks/status" + }, "TasksCapability": { "description": "Task capabilities shared by client and server.", "type": "object", @@ -2091,12 +2284,43 @@ } } }, + "TextContent": { + "description": "Text content block (spec `TextContent`).", + "type": "object", + "properties": { + "_meta": { + "description": "Optional protocol-level metadata for this content block.", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "annotations": { + "description": "Optional annotations describing how the client should use this content.", + "anyOf": [ + { + "$ref": "#/definitions/Annotations" + }, + { + "type": "null" + } + ] + }, + "text": { + "description": "The text content of the message.", + "type": "string" + } + }, + "required": [ + "text" + ] + }, "ToolResultContent": { "description": "Tool execution result in user message (SEP-1577).", "type": "object", "properties": { "_meta": { - "description": "Optional metadata", "type": [ "object", "null" @@ -2104,21 +2328,18 @@ "additionalProperties": true }, "content": { - "description": "Content blocks returned by the tool", "type": "array", "items": { - "$ref": "#/definitions/Annotated" + "$ref": "#/definitions/ContentBlock" } }, "isError": { - "description": "Whether tool execution failed", "type": [ "boolean", "null" ] }, "structuredContent": { - "description": "Optional structured result", "type": [ "object", "null" @@ -2126,7 +2347,6 @@ "additionalProperties": true }, "toolUseId": { - "description": "ID of the corresponding tool use", "type": "string" } }, @@ -2139,7 +2359,6 @@ "type": "object", "properties": { "_meta": { - "description": "Optional metadata (preserved for caching)", "type": [ "object", "null" @@ -2147,16 +2366,13 @@ "additionalProperties": true }, "id": { - "description": "Unique identifier for this tool call", "type": "string" }, "input": { - "description": "Input arguments for the tool", "type": "object", "additionalProperties": true }, "name": { - "description": "Name of the tool to call", "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 5cb0cc8f1..47d2ef835 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 @@ -37,188 +37,54 @@ } ], "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/RawAudioContent" - } - ], - "required": [ - "type" - ] - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "resource_link" - } - }, - "allOf": [ - { - "$ref": "#/definitions/RawResource" - } - ], - "required": [ - "type" - ] - } - ] - }, - "Annotated2": { - "description": "Represents a resource in the extension with metadata", + "Annotations": { + "description": "Optional annotations for the client. The client can use annotations to inform how objects are\nused or displayed.", "type": "object", "properties": { - "_meta": { - "description": "Optional additional metadata for this resource", - "type": [ - "object", - "null" - ], - "additionalProperties": true - }, - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "description": { - "description": "Optional description of the resource", - "type": [ - "string", - "null" - ] - }, - "icons": { - "description": "Optional list of icons for the resource", + "audience": { "type": [ "array", "null" ], "items": { - "$ref": "#/definitions/Icon" + "$ref": "#/definitions/Role" } }, - "mimeType": { - "description": "MIME type of the resource content (\"text\" or \"blob\")", + "lastModified": { "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 + "format": "date-time" }, - "title": { - "description": "Human-readable title of the resource", + "priority": { "type": [ - "string", + "number", "null" - ] - }, - "uri": { - "description": "URI representing the resource location (e.g., \"file:///path/to/file\" or \"str:///content\")", - "type": "string" + ], + "format": "float" } - }, - "required": [ - "uri", - "name" - ] + } + }, + "ArrayTypeConst": { + "type": "string", + "format": "const", + "const": "array" }, - "Annotated3": { + "AudioContent": { + "description": "Audio content with base64-encoded data (spec `AudioContent`).", "type": "object", "properties": { + "_meta": { + "description": "Optional protocol-level metadata for this content block.", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "annotations": { + "description": "Optional annotations describing how the client should use this content.", "anyOf": [ { "$ref": "#/definitions/Annotations" @@ -228,79 +94,20 @@ } ] }, - "description": { - "type": [ - "string", - "null" - ] - }, - "icons": { - "description": "Optional list of icons for the resource template", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Icon" - } - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "name": { + "data": { + "description": "The base64-encoded audio data.", "type": "string" }, - "title": { - "type": [ - "string", - "null" - ] - }, - "uriTemplate": { + "mimeType": { + "description": "The MIME type of the audio (e.g. `audio/wav`).", "type": "string" } }, "required": [ - "uriTemplate", - "name" + "data", + "mimeType" ] }, - "Annotations": { - "type": "object", - "properties": { - "audience": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Role" - } - }, - "lastModified": { - "type": [ - "string", - "null" - ], - "format": "date-time" - }, - "priority": { - "type": [ - "number", - "null" - ], - "format": "float" - } - } - }, - "ArrayTypeConst": { - "type": "string", - "format": "const", - "const": "array" - }, "BooleanSchema": { "description": "Schema definition for boolean properties.", "type": "object", @@ -361,7 +168,7 @@ "type": "array", "default": [], "items": { - "$ref": "#/definitions/Annotated" + "$ref": "#/definitions/ContentBlock" } }, "isError": { @@ -448,6 +255,13 @@ "CancelledNotificationParam": { "type": "object", "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "reason": { "type": [ "string", @@ -455,18 +269,29 @@ ] }, "requestId": { - "$ref": "#/definitions/NumberOrString" + "anyOf": [ + { + "$ref": "#/definitions/NumberOrString" + }, + { + "type": "null" + } + ] } - }, - "required": [ - "requestId" - ] + } }, "CompleteResult": { "type": "object", "properties": { - "completion": { - "$ref": "#/definitions/CompletionInfo" + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "completion": { + "$ref": "#/definitions/CompletionInfo" } }, "required": [ @@ -517,137 +342,114 @@ "title" ] }, - "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.", + "ContentBlock": { + "description": "Unified content block union (spec `ContentBlock`).\n\n`text | image | audio | resource_link | resource`", "oneOf": [ { - "description": "Include context from all connected MCP servers", - "type": "string", - "const": "allServers" + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "text" + } + }, + "allOf": [ + { + "$ref": "#/definitions/TextContent" + } + ], + "required": [ + "type" + ] }, { - "description": "Include no additional context", - "type": "string", - "const": "none" + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "image" + } + }, + "allOf": [ + { + "$ref": "#/definitions/ImageContent" + } + ], + "required": [ + "type" + ] }, - { - "description": "Include context only from the requesting server", - "type": "string", - "const": "thisServer" - } - ] - }, - "CreateElicitationRequestParams": { - "description": "Parameters for creating an elicitation request to gather user input.\n\nThis structure contains everything needed to request interactive input from a user:\n- A human-readable message explaining what information is needed\n- A type-safe schema defining the expected structure of the response\n\n# Example\n1. Form-based elicitation request\n```rust\nuse rmcp::model::*;\n\nlet params = CreateElicitationRequestParams::FormElicitationParams {\n meta: None,\n message: \"Please provide your email\".to_string(),\n requested_schema: ElicitationSchema::builder()\n .required_email(\"email\")\n .build()\n .unwrap(),\n};\n```\n2. URL-based elicitation request\n```rust\nuse rmcp::model::*;\nlet params = CreateElicitationRequestParams::UrlElicitationParams {\n meta: None,\n message: \"Please provide your feedback at the following URL\".to_string(),\n url: \"https://example.com/feedback\".to_string(),\n elicitation_id: \"unique-id-123\".to_string(),\n};\n```", - "anyOf": [ { "type": "object", "properties": { - "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true - }, - "message": { - "type": "string" - }, - "mode": { + "type": { "type": "string", - "const": "form" - }, - "requestedSchema": { - "$ref": "#/definitions/ElicitationSchema" + "const": "audio" } }, + "allOf": [ + { + "$ref": "#/definitions/AudioContent" + } + ], "required": [ - "mode", - "message", - "requestedSchema" + "type" ] }, { "type": "object", "properties": { - "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true - }, - "elicitationId": { - "type": "string" - }, - "message": { - "type": "string" - }, - "mode": { + "type": { "type": "string", - "const": "url" - }, - "url": { - "type": "string" + "const": "resource" } }, + "allOf": [ + { + "$ref": "#/definitions/EmbeddedResource" + } + ], "required": [ - "mode", - "message", - "url", - "elicitationId" + "type" ] }, { "type": "object", "properties": { - "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true - }, - "message": { - "type": "string" - }, - "requestedSchema": { - "$ref": "#/definitions/ElicitationSchema" + "type": { + "type": "string", + "const": "resource_link" } }, + "allOf": [ + { + "$ref": "#/definitions/Resource" + } + ], "required": [ - "message", - "requestedSchema" + "type" ] } ] }, - "CreateElicitationResult": { - "description": "The result returned by a client in response to an elicitation request.\n\nContains the user's decision (accept/decline/cancel) and optionally their input data\nif they chose to accept the request.", - "type": "object", - "properties": { - "_meta": { - "description": "Optional protocol-level metadata for this result.", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "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" }, - "action": { - "description": "The user's decision on how to handle the elicitation request", - "allOf": [ - { - "$ref": "#/definitions/ElicitationAction" - } - ] + { + "description": "Include no additional context", + "type": "string", + "const": "none" }, - "content": { - "description": "The actual data provided by the user, if they accepted the request.\nMust conform to the JSON schema specified in the original request.\nOnly present when action is Accept." + { + "description": "Include context only from the requesting server", + "type": "string", + "const": "thisServer" } - }, - "required": [ - "action" ] }, "CreateMessageRequestMethod": { @@ -724,11 +526,14 @@ }, "task": { "description": "Task metadata for async task management (SEP-1319)", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/TaskMetadata" + }, + { + "type": "null" + } + ] }, "temperature": { "description": "Temperature for controlling randomness (0.0 to 1.0)", @@ -806,40 +611,160 @@ "CustomResult": { "description": "A catch-all response either side can use for custom requests." }, - "ElicitationAction": { - "description": "Represents the possible actions a user can take in response to an elicitation request.\n\nWhen a server requests user input through elicitation, the user can:\n- Accept: Provide the requested information and continue\n- Decline: Refuse to provide the information but continue the operation\n- Cancel: Stop the entire operation", - "oneOf": [ + "ElicitRequestParams": { + "description": "Parameters for creating an elicitation request to gather user input.\n\nThis structure contains everything needed to request interactive input from a user:\n- A human-readable message explaining what information is needed\n- A type-safe schema defining the expected structure of the response\n\n# Example\n1. Form-based elicitation request\n```rust\nuse rmcp::model::*;\n\nlet params = ElicitRequestParams::FormElicitationParams {\n meta: None,\n message: \"Please provide your email\".to_string(),\n requested_schema: ElicitationSchema::builder()\n .required_email(\"email\")\n .build()\n .unwrap(),\n};\n```\n2. URL-based elicitation request\n```rust\nuse rmcp::model::*;\nlet params = ElicitRequestParams::UrlElicitationParams {\n meta: None,\n message: \"Please provide your feedback at the following URL\".to_string(),\n url: \"https://example.com/feedback\".to_string(),\n elicitation_id: \"unique-id-123\".to_string(),\n};\n```", + "anyOf": [ { - "description": "User accepts the request and provides the requested information", - "type": "string", - "const": "accept" + "type": "object", + "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "message": { + "type": "string" + }, + "mode": { + "type": "string", + "const": "form" + }, + "requestedSchema": { + "$ref": "#/definitions/ElicitationSchema" + } + }, + "required": [ + "mode", + "message", + "requestedSchema" + ] }, { - "description": "User declines to provide the information but allows the operation to continue", - "type": "string", - "const": "decline" + "type": "object", + "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "elicitationId": { + "type": "string" + }, + "message": { + "type": "string" + }, + "mode": { + "type": "string", + "const": "url" + }, + "url": { + "type": "string" + } + }, + "required": [ + "mode", + "message", + "url", + "elicitationId" + ] }, { - "description": "User cancels the entire operation", - "type": "string", - "const": "cancel" - } - ] - }, - "ElicitationCompletionNotificationMethod": { - "type": "string", - "format": "const", - "const": "notifications/elicitation/complete" - }, - "ElicitationCreateRequestMethod": { - "type": "string", - "format": "const", + "type": "object", + "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "message": { + "type": "string" + }, + "requestedSchema": { + "$ref": "#/definitions/ElicitationSchema" + } + }, + "required": [ + "message", + "requestedSchema" + ] + } + ] + }, + "ElicitResult": { + "description": "The result returned by a client in response to an elicitation request.\n\nContains the user's decision (accept/decline/cancel) and optionally their input data\nif they chose to accept the request.", + "type": "object", + "properties": { + "_meta": { + "description": "Optional protocol-level metadata for this result.", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "action": { + "description": "The user's decision on how to handle the elicitation request", + "allOf": [ + { + "$ref": "#/definitions/ElicitationAction" + } + ] + }, + "content": { + "description": "The actual data provided by the user, if they accepted the request.\nMust conform to the JSON schema specified in the original request.\nOnly present when action is Accept." + } + }, + "required": [ + "action" + ] + }, + "ElicitationAction": { + "description": "Represents the possible actions a user can take in response to an elicitation request.\n\nWhen a server requests user input through elicitation, the user can:\n- Accept: Provide the requested information and continue\n- Decline: Refuse to provide the information but continue the operation\n- Cancel: Stop the entire operation", + "oneOf": [ + { + "description": "User accepts the request and provides the requested information", + "type": "string", + "const": "accept" + }, + { + "description": "User declines to provide the information but allows the operation to continue", + "type": "string", + "const": "decline" + }, + { + "description": "User cancels the entire operation", + "type": "string", + "const": "cancel" + } + ] + }, + "ElicitationCompletionNotificationMethod": { + "type": "string", + "format": "const", + "const": "notifications/elicitation/complete" + }, + "ElicitationCreateRequestMethod": { + "type": "string", + "format": "const", "const": "elicitation/create" }, "ElicitationResponseNotificationParam": { "description": "Notification parameters for an url elicitation completion notification.", "type": "object", "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "elicitationId": { "type": "string" } @@ -863,7 +788,7 @@ "description": "Property definitions (must be primitive types)", "type": "object", "additionalProperties": { - "$ref": "#/definitions/PrimitiveSchema" + "$ref": "#/definitions/PrimitiveSchemaDefinition" } }, "required": { @@ -909,6 +834,42 @@ } } }, + "EmbeddedResource": { + "description": "Embedded resource content (spec `EmbeddedResource`).", + "type": "object", + "properties": { + "_meta": { + "description": "Optional protocol-level metadata for this content block.", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "annotations": { + "description": "Optional annotations describing how the client should use this content.", + "anyOf": [ + { + "$ref": "#/definitions/Annotations" + }, + { + "type": "null" + } + ] + }, + "resource": { + "description": "The embedded resource contents (text or blob).", + "allOf": [ + { + "$ref": "#/definitions/ResourceContents" + } + ] + } + }, + "required": [ + "resource" + ] + }, "EmptyObject": { "description": "This is commonly used for representing empty objects in MCP messages.\n\nwithout returning any specific data.", "type": "object", @@ -961,6 +922,13 @@ "GetPromptResult": { "type": "object", "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "description": { "type": [ "string", @@ -1101,6 +1069,43 @@ } ] }, + "ImageContent": { + "description": "Image content with base64-encoded data (spec `ImageContent`).", + "type": "object", + "properties": { + "_meta": { + "description": "Optional protocol-level metadata for this content block.", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "annotations": { + "description": "Optional annotations describing how the client should use this content.", + "anyOf": [ + { + "$ref": "#/definitions/Annotations" + }, + { + "type": "null" + } + ] + }, + "data": { + "description": "The base64-encoded image data.", + "type": "string" + }, + "mimeType": { + "description": "The MIME type of the image (e.g. `image/png`).", + "type": "string" + } + }, + "required": [ + "data", + "mimeType" + ] + }, "Implementation": { "type": "object", "properties": { @@ -1147,6 +1152,13 @@ "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": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "capabilities": { "description": "The capabilities this server provides (tools, resources, prompts, etc.)", "allOf": [ @@ -1302,6 +1314,9 @@ { "$ref": "#/definitions/Notification5" }, + { + "$ref": "#/definitions/Notification6" + }, { "$ref": "#/definitions/CustomNotification" } @@ -1370,6 +1385,12 @@ "description": "Legacy enum schema, keep for backward compatibility", "type": "object", "properties": { + "default": { + "type": [ + "string", + "null" + ] + }, "description": { "type": [ "string", @@ -1452,7 +1473,7 @@ "resourceTemplates": { "type": "array", "items": { - "$ref": "#/definitions/Annotated3" + "$ref": "#/definitions/ResourceTemplate" } } }, @@ -1479,7 +1500,7 @@ "resources": { "type": "array", "items": { - "$ref": "#/definitions/Annotated2" + "$ref": "#/definitions/Resource" } } }, @@ -1495,6 +1516,13 @@ "ListTasksResult": { "type": "object", "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "nextCursor": { "type": [ "string", @@ -1506,14 +1534,6 @@ "items": { "$ref": "#/definitions/Task" } - }, - "total": { - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0 } }, "required": [ @@ -1570,6 +1590,13 @@ "description": "Parameters for a logging message notification", "type": "object", "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "data": { "description": "The actual log data" }, @@ -1733,6 +1760,21 @@ "params" ] }, + "Notification6": { + "type": "object", + "properties": { + "method": { + "$ref": "#/definitions/TaskStatusNotificationMethod" + }, + "params": { + "$ref": "#/definitions/Task" + } + }, + "required": [ + "method", + "params" + ] + }, "NotificationNoParam": { "type": "object", "properties": { @@ -1846,7 +1888,7 @@ "format": "const", "const": "ping" }, - "PrimitiveSchema": { + "PrimitiveSchemaDefinition": { "description": "Primitive schema definition for elicitation properties.\n\nAccording to MCP 2025-06-18 specification, elicitation schemas must have\nproperties of primitive types only (string, number, integer, boolean, enum).\n\nNote: Put Enum as the first variant to avoid ambiguity during deserialization.\nThis is due to the fact that EnumSchema can contain StringSchema internally and serde\nuses first match wins strategy when deserializing untagged enums.", "anyOf": [ { @@ -1899,6 +1941,13 @@ "ProgressNotificationParam": { "type": "object", "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "message": { "description": "An optional message describing the current progress.", "type": [ @@ -1933,439 +1982,36 @@ "allOf": [ { "$ref": "#/definitions/NumberOrString" - } - ] - }, - "Prompt": { - "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": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/PromptArgument" - } - }, - "description": { - "description": "Optional description of what the prompt does", - "type": [ - "string", - "null" - ] - }, - "icons": { - "description": "Optional list of icons for the prompt", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Icon" - } - }, - "name": { - "description": "The name of the prompt", - "type": "string" - }, - "title": { - "type": [ - "string", - "null" - ] - } - }, - "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" - ] - }, - "title": { - "description": "A human-readable title for the argument", - "type": [ - "string", - "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": { - "_meta": { - "description": "Optional protocol-level metadata for this content block", - "type": [ - "object", - "null" - ], - "additionalProperties": true - }, - "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": "Audio content with base64-encoded data", - "type": "object", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string", - "const": "audio" - } - }, - "required": [ - "type", - "data", - "mimeType" - ] - }, - { - "description": "Embedded server-side resource", - "type": "object", - "properties": { - "_meta": { - "description": "Optional protocol-level metadata for this content block", - "type": [ - "object", - "null" - ], - "additionalProperties": true - }, - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "resource": { - "$ref": "#/definitions/ResourceContents" - }, - "type": { - "type": "string", - "const": "resource" - } - }, - "required": [ - "type", - "resource" - ] - }, - { - "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": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "description": { - "description": "Optional description of the resource", - "type": [ - "string", - "null" - ] - }, - "icons": { - "description": "Optional list of icons for the resource", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Icon" - } - }, - "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 - }, - "title": { - "description": "Human-readable title of the resource", - "type": [ - "string", - "null" - ] - }, - "type": { - "type": "string", - "const": "resource_link" - }, - "uri": { - "description": "URI representing the resource location (e.g., \"file:///path/to/file\" or \"str:///content\")", - "type": "string" - } - }, - "required": [ - "type", - "uri", - "name" - ] - } - ] - }, - "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" - }, - "RawAudioContent": { - "type": "object", - "properties": { - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType" - ] - }, - "RawEmbeddedResource": { - "type": "object", - "properties": { - "_meta": { - "description": "Optional protocol-level metadata for this content block", - "type": [ - "object", - "null" - ], - "additionalProperties": true - }, - "resource": { - "$ref": "#/definitions/ResourceContents" - } - }, - "required": [ - "resource" - ] - }, - "RawImageContent": { - "type": "object", - "properties": { - "_meta": { - "description": "Optional protocol-level metadata for this content block", - "type": [ - "object", - "null" - ], - "additionalProperties": true - }, - "data": { - "description": "The base64-encoded image", - "type": "string" - }, - "mimeType": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType" + } ] }, - "RawResource": { - "description": "Represents a resource in the extension with metadata", + "Prompt": { + "description": "A prompt or prompt template that the server offers (spec `Prompt`).", "type": "object", "properties": { "_meta": { - "description": "Optional additional metadata for this resource", "type": [ "object", "null" ], "additionalProperties": true }, + "arguments": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/PromptArgument" + } + }, "description": { - "description": "Optional description of the resource", "type": [ "string", "null" ] }, "icons": { - "description": "Optional list of icons for the resource", "type": [ "array", "null" @@ -2374,66 +2020,97 @@ "$ref": "#/definitions/Icon" } }, - "mimeType": { - "description": "MIME type of the resource content (\"text\" or \"blob\")", + "name": { + "type": "string" + }, + "title": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "name" + ] + }, + "PromptArgument": { + "description": "Describes an argument that a prompt can accept (spec `PromptArgument`).", + "type": "object", + "properties": { + "description": { "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", + "required": { "type": [ - "integer", + "boolean", "null" - ], - "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" } }, "required": [ - "uri", "name" ] }, - "RawTextContent": { + "PromptListChangedNotificationMethod": { + "type": "string", + "format": "const", + "const": "notifications/prompts/list_changed" + }, + "PromptMessage": { + "description": "A message returned as part of a prompt (spec `PromptMessage`).\n\nUses the unified `ContentBlock` for its content (text | image | audio | resource_link | resource).", "type": "object", "properties": { - "_meta": { - "description": "Optional protocol-level metadata for this content block", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "content": { + "$ref": "#/definitions/ContentBlock" }, - "text": { - "type": "string" + "role": { + "$ref": "#/definitions/Role" } }, "required": [ - "text" + "role", + "content" ] }, + "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" + }, "ReadResourceResult": { "description": "Result containing the contents of a read resource", "type": "object", "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "contents": { "description": "The actual content of the resource", "type": "array", @@ -2470,7 +2147,7 @@ "$ref": "#/definitions/ElicitationCreateRequestMethod" }, "params": { - "$ref": "#/definitions/CreateElicitationRequestParams" + "$ref": "#/definitions/ElicitRequestParams" } }, "required": [ @@ -2500,7 +2177,85 @@ "method" ] }, + "Resource": { + "description": "A known resource that the server is capable of reading (spec `Resource`).\n\nAlso used as the inner type of `ContentBlock::ResourceLink` (spec `ResourceLink extends Resource`).", + "type": "object", + "properties": { + "_meta": { + "description": "Optional protocol-level metadata for this resource.", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "annotations": { + "description": "Optional annotations describing how the client should use this resource.", + "anyOf": [ + { + "$ref": "#/definitions/Annotations" + }, + { + "type": "null" + } + ] + }, + "description": { + "description": "Optional description of what this resource represents.", + "type": [ + "string", + "null" + ] + }, + "icons": { + "description": "Optional set of icons the client may display for this resource.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Icon" + } + }, + "mimeType": { + "description": "The MIME type of this resource, if known.", + "type": [ + "string", + "null" + ] + }, + "name": { + "description": "The programmatic name of the resource.", + "type": "string" + }, + "size": { + "description": "The size of the raw resource content in bytes (before base64/tokenization), if known.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "title": { + "description": "Optional human-readable display title.", + "type": [ + "string", + "null" + ] + }, + "uri": { + "description": "The URI of this resource (e.g. `file:///path/to/file`).", + "type": "string" + } + }, + "required": [ + "uri", + "name" + ] + }, "ResourceContents": { + "description": "The contents of a specific resource or sub-resource.", "anyOf": [ { "type": "object", @@ -2565,6 +2320,74 @@ "format": "const", "const": "notifications/resources/list_changed" }, + "ResourceTemplate": { + "description": "A template description for resources available on the server (spec `ResourceTemplate`).", + "type": "object", + "properties": { + "_meta": { + "description": "Optional protocol-level metadata for this resource template.", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "annotations": { + "description": "Optional annotations describing how the client should use this template.", + "anyOf": [ + { + "$ref": "#/definitions/Annotations" + }, + { + "type": "null" + } + ] + }, + "description": { + "description": "Optional description of what this template is for.", + "type": [ + "string", + "null" + ] + }, + "icons": { + "description": "Optional set of icons the client may display for this template.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Icon" + } + }, + "mimeType": { + "description": "The MIME type for resources matching this template, if uniform.", + "type": [ + "string", + "null" + ] + }, + "name": { + "description": "The programmatic name of the resource template.", + "type": "string" + }, + "title": { + "description": "Optional human-readable display title.", + "type": [ + "string", + "null" + ] + }, + "uriTemplate": { + "description": "An RFC 6570 URI template for constructing resource URIs.", + "type": "string" + } + }, + "required": [ + "uriTemplate", + "name" + ] + }, "ResourceUpdatedNotificationMethod": { "type": "string", "format": "const", @@ -2574,6 +2397,13 @@ "description": "Parameters for a resource update notification", "type": "object", "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "uri": { "description": "The URI of the resource that was updated", "type": "string" @@ -2619,12 +2449,12 @@ "description": "Single or array content wrapper (SEP-1577).", "anyOf": [ { - "$ref": "#/definitions/SamplingMessageContent" + "$ref": "#/definitions/SamplingMessageContentBlock" }, { "type": "array", "items": { - "$ref": "#/definitions/SamplingMessageContent" + "$ref": "#/definitions/SamplingMessageContentBlock" } } ] @@ -2662,7 +2492,7 @@ "content" ] }, - "SamplingMessageContent": { + "SamplingMessageContentBlock": { "description": "Content types for sampling messages (SEP-1577).", "oneOf": [ { @@ -2675,7 +2505,7 @@ }, "allOf": [ { - "$ref": "#/definitions/RawTextContent" + "$ref": "#/definitions/TextContent" } ], "required": [ @@ -2692,7 +2522,7 @@ }, "allOf": [ { - "$ref": "#/definitions/RawImageContent" + "$ref": "#/definitions/ImageContent" } ], "required": [ @@ -2709,7 +2539,7 @@ }, "allOf": [ { - "$ref": "#/definitions/RawAudioContent" + "$ref": "#/definitions/AudioContent" } ], "required": [ @@ -2877,7 +2707,7 @@ "$ref": "#/definitions/ListToolsResult" }, { - "$ref": "#/definitions/CreateElicitationResult" + "$ref": "#/definitions/ElicitResult" }, { "$ref": "#/definitions/CreateTaskResult" @@ -3070,6 +2900,20 @@ "lastUpdatedAt" ] }, + "TaskMetadata": { + "description": "Metadata for augmenting a request with task execution (spec `TaskMetadata`).", + "type": "object", + "properties": { + "ttl": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + } + } + }, "TaskRequestsCapability": { "description": "Request types that support task-augmented execution.", "type": "object", @@ -3136,6 +2980,11 @@ } ] }, + "TaskStatusNotificationMethod": { + "type": "string", + "format": "const", + "const": "notifications/tasks/status" + }, "TaskSupport": { "description": "Per-tool task support mode as defined in the MCP specification.\n\nThis enum indicates whether a tool supports task-based invocation,\nallowing clients to know how to properly call the tool.\n\nSee [Tool-Level Negotiation](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#tool-level-negotiation).", "oneOf": [ @@ -3186,6 +3035,38 @@ } } }, + "TextContent": { + "description": "Text content block (spec `TextContent`).", + "type": "object", + "properties": { + "_meta": { + "description": "Optional protocol-level metadata for this content block.", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "annotations": { + "description": "Optional annotations describing how the client should use this content.", + "anyOf": [ + { + "$ref": "#/definitions/Annotations" + }, + { + "type": "null" + } + ] + }, + "text": { + "description": "The text content of the message.", + "type": "string" + } + }, + "required": [ + "text" + ] + }, "TitledItems": { "description": "Items for titled multi-select options", "type": "object", @@ -3476,7 +3357,6 @@ "type": "object", "properties": { "_meta": { - "description": "Optional metadata", "type": [ "object", "null" @@ -3484,21 +3364,18 @@ "additionalProperties": true }, "content": { - "description": "Content blocks returned by the tool", "type": "array", "items": { - "$ref": "#/definitions/Annotated" + "$ref": "#/definitions/ContentBlock" } }, "isError": { - "description": "Whether tool execution failed", "type": [ "boolean", "null" ] }, "structuredContent": { - "description": "Optional structured result", "type": [ "object", "null" @@ -3506,7 +3383,6 @@ "additionalProperties": true }, "toolUseId": { - "description": "ID of the corresponding tool use", "type": "string" } }, @@ -3519,7 +3395,6 @@ "type": "object", "properties": { "_meta": { - "description": "Optional metadata (preserved for caching)", "type": [ "object", "null" @@ -3527,16 +3402,13 @@ "additionalProperties": true }, "id": { - "description": "Unique identifier for this tool call", "type": "string" }, "input": { - "description": "Input arguments for the tool", "type": "object", "additionalProperties": true }, "name": { - "description": "Name of the tool to call", "type": "string" } }, 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 5cb0cc8f1..47d2ef835 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 @@ -37,188 +37,54 @@ } ], "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/RawAudioContent" - } - ], - "required": [ - "type" - ] - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "resource_link" - } - }, - "allOf": [ - { - "$ref": "#/definitions/RawResource" - } - ], - "required": [ - "type" - ] - } - ] - }, - "Annotated2": { - "description": "Represents a resource in the extension with metadata", + "Annotations": { + "description": "Optional annotations for the client. The client can use annotations to inform how objects are\nused or displayed.", "type": "object", "properties": { - "_meta": { - "description": "Optional additional metadata for this resource", - "type": [ - "object", - "null" - ], - "additionalProperties": true - }, - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "description": { - "description": "Optional description of the resource", - "type": [ - "string", - "null" - ] - }, - "icons": { - "description": "Optional list of icons for the resource", + "audience": { "type": [ "array", "null" ], "items": { - "$ref": "#/definitions/Icon" + "$ref": "#/definitions/Role" } }, - "mimeType": { - "description": "MIME type of the resource content (\"text\" or \"blob\")", + "lastModified": { "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 + "format": "date-time" }, - "title": { - "description": "Human-readable title of the resource", + "priority": { "type": [ - "string", + "number", "null" - ] - }, - "uri": { - "description": "URI representing the resource location (e.g., \"file:///path/to/file\" or \"str:///content\")", - "type": "string" + ], + "format": "float" } - }, - "required": [ - "uri", - "name" - ] + } + }, + "ArrayTypeConst": { + "type": "string", + "format": "const", + "const": "array" }, - "Annotated3": { + "AudioContent": { + "description": "Audio content with base64-encoded data (spec `AudioContent`).", "type": "object", "properties": { + "_meta": { + "description": "Optional protocol-level metadata for this content block.", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "annotations": { + "description": "Optional annotations describing how the client should use this content.", "anyOf": [ { "$ref": "#/definitions/Annotations" @@ -228,79 +94,20 @@ } ] }, - "description": { - "type": [ - "string", - "null" - ] - }, - "icons": { - "description": "Optional list of icons for the resource template", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Icon" - } - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "name": { + "data": { + "description": "The base64-encoded audio data.", "type": "string" }, - "title": { - "type": [ - "string", - "null" - ] - }, - "uriTemplate": { + "mimeType": { + "description": "The MIME type of the audio (e.g. `audio/wav`).", "type": "string" } }, "required": [ - "uriTemplate", - "name" + "data", + "mimeType" ] }, - "Annotations": { - "type": "object", - "properties": { - "audience": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Role" - } - }, - "lastModified": { - "type": [ - "string", - "null" - ], - "format": "date-time" - }, - "priority": { - "type": [ - "number", - "null" - ], - "format": "float" - } - } - }, - "ArrayTypeConst": { - "type": "string", - "format": "const", - "const": "array" - }, "BooleanSchema": { "description": "Schema definition for boolean properties.", "type": "object", @@ -361,7 +168,7 @@ "type": "array", "default": [], "items": { - "$ref": "#/definitions/Annotated" + "$ref": "#/definitions/ContentBlock" } }, "isError": { @@ -448,6 +255,13 @@ "CancelledNotificationParam": { "type": "object", "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "reason": { "type": [ "string", @@ -455,18 +269,29 @@ ] }, "requestId": { - "$ref": "#/definitions/NumberOrString" + "anyOf": [ + { + "$ref": "#/definitions/NumberOrString" + }, + { + "type": "null" + } + ] } - }, - "required": [ - "requestId" - ] + } }, "CompleteResult": { "type": "object", "properties": { - "completion": { - "$ref": "#/definitions/CompletionInfo" + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "completion": { + "$ref": "#/definitions/CompletionInfo" } }, "required": [ @@ -517,137 +342,114 @@ "title" ] }, - "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.", + "ContentBlock": { + "description": "Unified content block union (spec `ContentBlock`).\n\n`text | image | audio | resource_link | resource`", "oneOf": [ { - "description": "Include context from all connected MCP servers", - "type": "string", - "const": "allServers" + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "text" + } + }, + "allOf": [ + { + "$ref": "#/definitions/TextContent" + } + ], + "required": [ + "type" + ] }, { - "description": "Include no additional context", - "type": "string", - "const": "none" + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "image" + } + }, + "allOf": [ + { + "$ref": "#/definitions/ImageContent" + } + ], + "required": [ + "type" + ] }, - { - "description": "Include context only from the requesting server", - "type": "string", - "const": "thisServer" - } - ] - }, - "CreateElicitationRequestParams": { - "description": "Parameters for creating an elicitation request to gather user input.\n\nThis structure contains everything needed to request interactive input from a user:\n- A human-readable message explaining what information is needed\n- A type-safe schema defining the expected structure of the response\n\n# Example\n1. Form-based elicitation request\n```rust\nuse rmcp::model::*;\n\nlet params = CreateElicitationRequestParams::FormElicitationParams {\n meta: None,\n message: \"Please provide your email\".to_string(),\n requested_schema: ElicitationSchema::builder()\n .required_email(\"email\")\n .build()\n .unwrap(),\n};\n```\n2. URL-based elicitation request\n```rust\nuse rmcp::model::*;\nlet params = CreateElicitationRequestParams::UrlElicitationParams {\n meta: None,\n message: \"Please provide your feedback at the following URL\".to_string(),\n url: \"https://example.com/feedback\".to_string(),\n elicitation_id: \"unique-id-123\".to_string(),\n};\n```", - "anyOf": [ { "type": "object", "properties": { - "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true - }, - "message": { - "type": "string" - }, - "mode": { + "type": { "type": "string", - "const": "form" - }, - "requestedSchema": { - "$ref": "#/definitions/ElicitationSchema" + "const": "audio" } }, + "allOf": [ + { + "$ref": "#/definitions/AudioContent" + } + ], "required": [ - "mode", - "message", - "requestedSchema" + "type" ] }, { "type": "object", "properties": { - "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true - }, - "elicitationId": { - "type": "string" - }, - "message": { - "type": "string" - }, - "mode": { + "type": { "type": "string", - "const": "url" - }, - "url": { - "type": "string" + "const": "resource" } }, + "allOf": [ + { + "$ref": "#/definitions/EmbeddedResource" + } + ], "required": [ - "mode", - "message", - "url", - "elicitationId" + "type" ] }, { "type": "object", "properties": { - "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true - }, - "message": { - "type": "string" - }, - "requestedSchema": { - "$ref": "#/definitions/ElicitationSchema" + "type": { + "type": "string", + "const": "resource_link" } }, + "allOf": [ + { + "$ref": "#/definitions/Resource" + } + ], "required": [ - "message", - "requestedSchema" + "type" ] } ] }, - "CreateElicitationResult": { - "description": "The result returned by a client in response to an elicitation request.\n\nContains the user's decision (accept/decline/cancel) and optionally their input data\nif they chose to accept the request.", - "type": "object", - "properties": { - "_meta": { - "description": "Optional protocol-level metadata for this result.", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "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" }, - "action": { - "description": "The user's decision on how to handle the elicitation request", - "allOf": [ - { - "$ref": "#/definitions/ElicitationAction" - } - ] + { + "description": "Include no additional context", + "type": "string", + "const": "none" }, - "content": { - "description": "The actual data provided by the user, if they accepted the request.\nMust conform to the JSON schema specified in the original request.\nOnly present when action is Accept." + { + "description": "Include context only from the requesting server", + "type": "string", + "const": "thisServer" } - }, - "required": [ - "action" ] }, "CreateMessageRequestMethod": { @@ -724,11 +526,14 @@ }, "task": { "description": "Task metadata for async task management (SEP-1319)", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/TaskMetadata" + }, + { + "type": "null" + } + ] }, "temperature": { "description": "Temperature for controlling randomness (0.0 to 1.0)", @@ -806,40 +611,160 @@ "CustomResult": { "description": "A catch-all response either side can use for custom requests." }, - "ElicitationAction": { - "description": "Represents the possible actions a user can take in response to an elicitation request.\n\nWhen a server requests user input through elicitation, the user can:\n- Accept: Provide the requested information and continue\n- Decline: Refuse to provide the information but continue the operation\n- Cancel: Stop the entire operation", - "oneOf": [ + "ElicitRequestParams": { + "description": "Parameters for creating an elicitation request to gather user input.\n\nThis structure contains everything needed to request interactive input from a user:\n- A human-readable message explaining what information is needed\n- A type-safe schema defining the expected structure of the response\n\n# Example\n1. Form-based elicitation request\n```rust\nuse rmcp::model::*;\n\nlet params = ElicitRequestParams::FormElicitationParams {\n meta: None,\n message: \"Please provide your email\".to_string(),\n requested_schema: ElicitationSchema::builder()\n .required_email(\"email\")\n .build()\n .unwrap(),\n};\n```\n2. URL-based elicitation request\n```rust\nuse rmcp::model::*;\nlet params = ElicitRequestParams::UrlElicitationParams {\n meta: None,\n message: \"Please provide your feedback at the following URL\".to_string(),\n url: \"https://example.com/feedback\".to_string(),\n elicitation_id: \"unique-id-123\".to_string(),\n};\n```", + "anyOf": [ { - "description": "User accepts the request and provides the requested information", - "type": "string", - "const": "accept" + "type": "object", + "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "message": { + "type": "string" + }, + "mode": { + "type": "string", + "const": "form" + }, + "requestedSchema": { + "$ref": "#/definitions/ElicitationSchema" + } + }, + "required": [ + "mode", + "message", + "requestedSchema" + ] }, { - "description": "User declines to provide the information but allows the operation to continue", - "type": "string", - "const": "decline" + "type": "object", + "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "elicitationId": { + "type": "string" + }, + "message": { + "type": "string" + }, + "mode": { + "type": "string", + "const": "url" + }, + "url": { + "type": "string" + } + }, + "required": [ + "mode", + "message", + "url", + "elicitationId" + ] }, { - "description": "User cancels the entire operation", - "type": "string", - "const": "cancel" - } - ] - }, - "ElicitationCompletionNotificationMethod": { - "type": "string", - "format": "const", - "const": "notifications/elicitation/complete" - }, - "ElicitationCreateRequestMethod": { - "type": "string", - "format": "const", + "type": "object", + "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "message": { + "type": "string" + }, + "requestedSchema": { + "$ref": "#/definitions/ElicitationSchema" + } + }, + "required": [ + "message", + "requestedSchema" + ] + } + ] + }, + "ElicitResult": { + "description": "The result returned by a client in response to an elicitation request.\n\nContains the user's decision (accept/decline/cancel) and optionally their input data\nif they chose to accept the request.", + "type": "object", + "properties": { + "_meta": { + "description": "Optional protocol-level metadata for this result.", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "action": { + "description": "The user's decision on how to handle the elicitation request", + "allOf": [ + { + "$ref": "#/definitions/ElicitationAction" + } + ] + }, + "content": { + "description": "The actual data provided by the user, if they accepted the request.\nMust conform to the JSON schema specified in the original request.\nOnly present when action is Accept." + } + }, + "required": [ + "action" + ] + }, + "ElicitationAction": { + "description": "Represents the possible actions a user can take in response to an elicitation request.\n\nWhen a server requests user input through elicitation, the user can:\n- Accept: Provide the requested information and continue\n- Decline: Refuse to provide the information but continue the operation\n- Cancel: Stop the entire operation", + "oneOf": [ + { + "description": "User accepts the request and provides the requested information", + "type": "string", + "const": "accept" + }, + { + "description": "User declines to provide the information but allows the operation to continue", + "type": "string", + "const": "decline" + }, + { + "description": "User cancels the entire operation", + "type": "string", + "const": "cancel" + } + ] + }, + "ElicitationCompletionNotificationMethod": { + "type": "string", + "format": "const", + "const": "notifications/elicitation/complete" + }, + "ElicitationCreateRequestMethod": { + "type": "string", + "format": "const", "const": "elicitation/create" }, "ElicitationResponseNotificationParam": { "description": "Notification parameters for an url elicitation completion notification.", "type": "object", "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "elicitationId": { "type": "string" } @@ -863,7 +788,7 @@ "description": "Property definitions (must be primitive types)", "type": "object", "additionalProperties": { - "$ref": "#/definitions/PrimitiveSchema" + "$ref": "#/definitions/PrimitiveSchemaDefinition" } }, "required": { @@ -909,6 +834,42 @@ } } }, + "EmbeddedResource": { + "description": "Embedded resource content (spec `EmbeddedResource`).", + "type": "object", + "properties": { + "_meta": { + "description": "Optional protocol-level metadata for this content block.", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "annotations": { + "description": "Optional annotations describing how the client should use this content.", + "anyOf": [ + { + "$ref": "#/definitions/Annotations" + }, + { + "type": "null" + } + ] + }, + "resource": { + "description": "The embedded resource contents (text or blob).", + "allOf": [ + { + "$ref": "#/definitions/ResourceContents" + } + ] + } + }, + "required": [ + "resource" + ] + }, "EmptyObject": { "description": "This is commonly used for representing empty objects in MCP messages.\n\nwithout returning any specific data.", "type": "object", @@ -961,6 +922,13 @@ "GetPromptResult": { "type": "object", "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "description": { "type": [ "string", @@ -1101,6 +1069,43 @@ } ] }, + "ImageContent": { + "description": "Image content with base64-encoded data (spec `ImageContent`).", + "type": "object", + "properties": { + "_meta": { + "description": "Optional protocol-level metadata for this content block.", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "annotations": { + "description": "Optional annotations describing how the client should use this content.", + "anyOf": [ + { + "$ref": "#/definitions/Annotations" + }, + { + "type": "null" + } + ] + }, + "data": { + "description": "The base64-encoded image data.", + "type": "string" + }, + "mimeType": { + "description": "The MIME type of the image (e.g. `image/png`).", + "type": "string" + } + }, + "required": [ + "data", + "mimeType" + ] + }, "Implementation": { "type": "object", "properties": { @@ -1147,6 +1152,13 @@ "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": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "capabilities": { "description": "The capabilities this server provides (tools, resources, prompts, etc.)", "allOf": [ @@ -1302,6 +1314,9 @@ { "$ref": "#/definitions/Notification5" }, + { + "$ref": "#/definitions/Notification6" + }, { "$ref": "#/definitions/CustomNotification" } @@ -1370,6 +1385,12 @@ "description": "Legacy enum schema, keep for backward compatibility", "type": "object", "properties": { + "default": { + "type": [ + "string", + "null" + ] + }, "description": { "type": [ "string", @@ -1452,7 +1473,7 @@ "resourceTemplates": { "type": "array", "items": { - "$ref": "#/definitions/Annotated3" + "$ref": "#/definitions/ResourceTemplate" } } }, @@ -1479,7 +1500,7 @@ "resources": { "type": "array", "items": { - "$ref": "#/definitions/Annotated2" + "$ref": "#/definitions/Resource" } } }, @@ -1495,6 +1516,13 @@ "ListTasksResult": { "type": "object", "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "nextCursor": { "type": [ "string", @@ -1506,14 +1534,6 @@ "items": { "$ref": "#/definitions/Task" } - }, - "total": { - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0 } }, "required": [ @@ -1570,6 +1590,13 @@ "description": "Parameters for a logging message notification", "type": "object", "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "data": { "description": "The actual log data" }, @@ -1733,6 +1760,21 @@ "params" ] }, + "Notification6": { + "type": "object", + "properties": { + "method": { + "$ref": "#/definitions/TaskStatusNotificationMethod" + }, + "params": { + "$ref": "#/definitions/Task" + } + }, + "required": [ + "method", + "params" + ] + }, "NotificationNoParam": { "type": "object", "properties": { @@ -1846,7 +1888,7 @@ "format": "const", "const": "ping" }, - "PrimitiveSchema": { + "PrimitiveSchemaDefinition": { "description": "Primitive schema definition for elicitation properties.\n\nAccording to MCP 2025-06-18 specification, elicitation schemas must have\nproperties of primitive types only (string, number, integer, boolean, enum).\n\nNote: Put Enum as the first variant to avoid ambiguity during deserialization.\nThis is due to the fact that EnumSchema can contain StringSchema internally and serde\nuses first match wins strategy when deserializing untagged enums.", "anyOf": [ { @@ -1899,6 +1941,13 @@ "ProgressNotificationParam": { "type": "object", "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "message": { "description": "An optional message describing the current progress.", "type": [ @@ -1933,439 +1982,36 @@ "allOf": [ { "$ref": "#/definitions/NumberOrString" - } - ] - }, - "Prompt": { - "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": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/PromptArgument" - } - }, - "description": { - "description": "Optional description of what the prompt does", - "type": [ - "string", - "null" - ] - }, - "icons": { - "description": "Optional list of icons for the prompt", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Icon" - } - }, - "name": { - "description": "The name of the prompt", - "type": "string" - }, - "title": { - "type": [ - "string", - "null" - ] - } - }, - "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" - ] - }, - "title": { - "description": "A human-readable title for the argument", - "type": [ - "string", - "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": { - "_meta": { - "description": "Optional protocol-level metadata for this content block", - "type": [ - "object", - "null" - ], - "additionalProperties": true - }, - "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": "Audio content with base64-encoded data", - "type": "object", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string", - "const": "audio" - } - }, - "required": [ - "type", - "data", - "mimeType" - ] - }, - { - "description": "Embedded server-side resource", - "type": "object", - "properties": { - "_meta": { - "description": "Optional protocol-level metadata for this content block", - "type": [ - "object", - "null" - ], - "additionalProperties": true - }, - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "resource": { - "$ref": "#/definitions/ResourceContents" - }, - "type": { - "type": "string", - "const": "resource" - } - }, - "required": [ - "type", - "resource" - ] - }, - { - "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": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "description": { - "description": "Optional description of the resource", - "type": [ - "string", - "null" - ] - }, - "icons": { - "description": "Optional list of icons for the resource", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Icon" - } - }, - "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 - }, - "title": { - "description": "Human-readable title of the resource", - "type": [ - "string", - "null" - ] - }, - "type": { - "type": "string", - "const": "resource_link" - }, - "uri": { - "description": "URI representing the resource location (e.g., \"file:///path/to/file\" or \"str:///content\")", - "type": "string" - } - }, - "required": [ - "type", - "uri", - "name" - ] - } - ] - }, - "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" - }, - "RawAudioContent": { - "type": "object", - "properties": { - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType" - ] - }, - "RawEmbeddedResource": { - "type": "object", - "properties": { - "_meta": { - "description": "Optional protocol-level metadata for this content block", - "type": [ - "object", - "null" - ], - "additionalProperties": true - }, - "resource": { - "$ref": "#/definitions/ResourceContents" - } - }, - "required": [ - "resource" - ] - }, - "RawImageContent": { - "type": "object", - "properties": { - "_meta": { - "description": "Optional protocol-level metadata for this content block", - "type": [ - "object", - "null" - ], - "additionalProperties": true - }, - "data": { - "description": "The base64-encoded image", - "type": "string" - }, - "mimeType": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType" + } ] }, - "RawResource": { - "description": "Represents a resource in the extension with metadata", + "Prompt": { + "description": "A prompt or prompt template that the server offers (spec `Prompt`).", "type": "object", "properties": { "_meta": { - "description": "Optional additional metadata for this resource", "type": [ "object", "null" ], "additionalProperties": true }, + "arguments": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/PromptArgument" + } + }, "description": { - "description": "Optional description of the resource", "type": [ "string", "null" ] }, "icons": { - "description": "Optional list of icons for the resource", "type": [ "array", "null" @@ -2374,66 +2020,97 @@ "$ref": "#/definitions/Icon" } }, - "mimeType": { - "description": "MIME type of the resource content (\"text\" or \"blob\")", + "name": { + "type": "string" + }, + "title": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "name" + ] + }, + "PromptArgument": { + "description": "Describes an argument that a prompt can accept (spec `PromptArgument`).", + "type": "object", + "properties": { + "description": { "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", + "required": { "type": [ - "integer", + "boolean", "null" - ], - "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" } }, "required": [ - "uri", "name" ] }, - "RawTextContent": { + "PromptListChangedNotificationMethod": { + "type": "string", + "format": "const", + "const": "notifications/prompts/list_changed" + }, + "PromptMessage": { + "description": "A message returned as part of a prompt (spec `PromptMessage`).\n\nUses the unified `ContentBlock` for its content (text | image | audio | resource_link | resource).", "type": "object", "properties": { - "_meta": { - "description": "Optional protocol-level metadata for this content block", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "content": { + "$ref": "#/definitions/ContentBlock" }, - "text": { - "type": "string" + "role": { + "$ref": "#/definitions/Role" } }, "required": [ - "text" + "role", + "content" ] }, + "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" + }, "ReadResourceResult": { "description": "Result containing the contents of a read resource", "type": "object", "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "contents": { "description": "The actual content of the resource", "type": "array", @@ -2470,7 +2147,7 @@ "$ref": "#/definitions/ElicitationCreateRequestMethod" }, "params": { - "$ref": "#/definitions/CreateElicitationRequestParams" + "$ref": "#/definitions/ElicitRequestParams" } }, "required": [ @@ -2500,7 +2177,85 @@ "method" ] }, + "Resource": { + "description": "A known resource that the server is capable of reading (spec `Resource`).\n\nAlso used as the inner type of `ContentBlock::ResourceLink` (spec `ResourceLink extends Resource`).", + "type": "object", + "properties": { + "_meta": { + "description": "Optional protocol-level metadata for this resource.", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "annotations": { + "description": "Optional annotations describing how the client should use this resource.", + "anyOf": [ + { + "$ref": "#/definitions/Annotations" + }, + { + "type": "null" + } + ] + }, + "description": { + "description": "Optional description of what this resource represents.", + "type": [ + "string", + "null" + ] + }, + "icons": { + "description": "Optional set of icons the client may display for this resource.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Icon" + } + }, + "mimeType": { + "description": "The MIME type of this resource, if known.", + "type": [ + "string", + "null" + ] + }, + "name": { + "description": "The programmatic name of the resource.", + "type": "string" + }, + "size": { + "description": "The size of the raw resource content in bytes (before base64/tokenization), if known.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "title": { + "description": "Optional human-readable display title.", + "type": [ + "string", + "null" + ] + }, + "uri": { + "description": "The URI of this resource (e.g. `file:///path/to/file`).", + "type": "string" + } + }, + "required": [ + "uri", + "name" + ] + }, "ResourceContents": { + "description": "The contents of a specific resource or sub-resource.", "anyOf": [ { "type": "object", @@ -2565,6 +2320,74 @@ "format": "const", "const": "notifications/resources/list_changed" }, + "ResourceTemplate": { + "description": "A template description for resources available on the server (spec `ResourceTemplate`).", + "type": "object", + "properties": { + "_meta": { + "description": "Optional protocol-level metadata for this resource template.", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "annotations": { + "description": "Optional annotations describing how the client should use this template.", + "anyOf": [ + { + "$ref": "#/definitions/Annotations" + }, + { + "type": "null" + } + ] + }, + "description": { + "description": "Optional description of what this template is for.", + "type": [ + "string", + "null" + ] + }, + "icons": { + "description": "Optional set of icons the client may display for this template.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Icon" + } + }, + "mimeType": { + "description": "The MIME type for resources matching this template, if uniform.", + "type": [ + "string", + "null" + ] + }, + "name": { + "description": "The programmatic name of the resource template.", + "type": "string" + }, + "title": { + "description": "Optional human-readable display title.", + "type": [ + "string", + "null" + ] + }, + "uriTemplate": { + "description": "An RFC 6570 URI template for constructing resource URIs.", + "type": "string" + } + }, + "required": [ + "uriTemplate", + "name" + ] + }, "ResourceUpdatedNotificationMethod": { "type": "string", "format": "const", @@ -2574,6 +2397,13 @@ "description": "Parameters for a resource update notification", "type": "object", "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "uri": { "description": "The URI of the resource that was updated", "type": "string" @@ -2619,12 +2449,12 @@ "description": "Single or array content wrapper (SEP-1577).", "anyOf": [ { - "$ref": "#/definitions/SamplingMessageContent" + "$ref": "#/definitions/SamplingMessageContentBlock" }, { "type": "array", "items": { - "$ref": "#/definitions/SamplingMessageContent" + "$ref": "#/definitions/SamplingMessageContentBlock" } } ] @@ -2662,7 +2492,7 @@ "content" ] }, - "SamplingMessageContent": { + "SamplingMessageContentBlock": { "description": "Content types for sampling messages (SEP-1577).", "oneOf": [ { @@ -2675,7 +2505,7 @@ }, "allOf": [ { - "$ref": "#/definitions/RawTextContent" + "$ref": "#/definitions/TextContent" } ], "required": [ @@ -2692,7 +2522,7 @@ }, "allOf": [ { - "$ref": "#/definitions/RawImageContent" + "$ref": "#/definitions/ImageContent" } ], "required": [ @@ -2709,7 +2539,7 @@ }, "allOf": [ { - "$ref": "#/definitions/RawAudioContent" + "$ref": "#/definitions/AudioContent" } ], "required": [ @@ -2877,7 +2707,7 @@ "$ref": "#/definitions/ListToolsResult" }, { - "$ref": "#/definitions/CreateElicitationResult" + "$ref": "#/definitions/ElicitResult" }, { "$ref": "#/definitions/CreateTaskResult" @@ -3070,6 +2900,20 @@ "lastUpdatedAt" ] }, + "TaskMetadata": { + "description": "Metadata for augmenting a request with task execution (spec `TaskMetadata`).", + "type": "object", + "properties": { + "ttl": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + } + } + }, "TaskRequestsCapability": { "description": "Request types that support task-augmented execution.", "type": "object", @@ -3136,6 +2980,11 @@ } ] }, + "TaskStatusNotificationMethod": { + "type": "string", + "format": "const", + "const": "notifications/tasks/status" + }, "TaskSupport": { "description": "Per-tool task support mode as defined in the MCP specification.\n\nThis enum indicates whether a tool supports task-based invocation,\nallowing clients to know how to properly call the tool.\n\nSee [Tool-Level Negotiation](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#tool-level-negotiation).", "oneOf": [ @@ -3186,6 +3035,38 @@ } } }, + "TextContent": { + "description": "Text content block (spec `TextContent`).", + "type": "object", + "properties": { + "_meta": { + "description": "Optional protocol-level metadata for this content block.", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "annotations": { + "description": "Optional annotations describing how the client should use this content.", + "anyOf": [ + { + "$ref": "#/definitions/Annotations" + }, + { + "type": "null" + } + ] + }, + "text": { + "description": "The text content of the message.", + "type": "string" + } + }, + "required": [ + "text" + ] + }, "TitledItems": { "description": "Items for titled multi-select options", "type": "object", @@ -3476,7 +3357,6 @@ "type": "object", "properties": { "_meta": { - "description": "Optional metadata", "type": [ "object", "null" @@ -3484,21 +3364,18 @@ "additionalProperties": true }, "content": { - "description": "Content blocks returned by the tool", "type": "array", "items": { - "$ref": "#/definitions/Annotated" + "$ref": "#/definitions/ContentBlock" } }, "isError": { - "description": "Whether tool execution failed", "type": [ "boolean", "null" ] }, "structuredContent": { - "description": "Optional structured result", "type": [ "object", "null" @@ -3506,7 +3383,6 @@ "additionalProperties": true }, "toolUseId": { - "description": "ID of the corresponding tool use", "type": "string" } }, @@ -3519,7 +3395,6 @@ "type": "object", "properties": { "_meta": { - "description": "Optional metadata (preserved for caching)", "type": [ "object", "null" @@ -3527,16 +3402,13 @@ "additionalProperties": true }, "id": { - "description": "Unique identifier for this tool call", "type": "string" }, "input": { - "description": "Input arguments for the tool", "type": "object", "additionalProperties": true }, "name": { - "description": "Name of the tool to call", "type": "string" } }, diff --git a/crates/rmcp/tests/test_notification.rs b/crates/rmcp/tests/test_notification.rs index 662c4bd58..db4cf33f8 100644 --- a/crates/rmcp/tests/test_notification.rs +++ b/crates/rmcp/tests/test_notification.rs @@ -38,7 +38,7 @@ impl ServerHandler for Server { let _enter = span.enter(); if let Err(e) = peer - .notify_resource_updated(ResourceUpdatedNotificationParam { uri: uri.clone() }) + .notify_resource_updated(ResourceUpdatedNotificationParam::new(uri.clone())) .await { panic!("Failed to send notification: {}", e); diff --git a/crates/rmcp/tests/test_progress_subscriber.rs b/crates/rmcp/tests/test_progress_subscriber.rs index 7bed457f4..5df8a7b71 100644 --- a/crates/rmcp/tests/test_progress_subscriber.rs +++ b/crates/rmcp/tests/test_progress_subscriber.rs @@ -71,12 +71,11 @@ impl MyServer { ))?; for step in 0..10 { let _ = client - .notify_progress(ProgressNotificationParam { - progress_token: progress_token.clone(), - progress: (step as f64), - total: Some(10.0), - message: Some("Some message".into()), - }) + .notify_progress( + ProgressNotificationParam::new(progress_token.clone(), step as f64) + .with_total(10.0) + .with_message("Some message"), + ) .await; tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; } diff --git a/crates/rmcp/tests/test_prompt_macro_annotations.rs b/crates/rmcp/tests/test_prompt_macro_annotations.rs index caa017936..b5b6ef6af 100644 --- a/crates/rmcp/tests/test_prompt_macro_annotations.rs +++ b/crates/rmcp/tests/test_prompt_macro_annotations.rs @@ -4,7 +4,7 @@ use rmcp::{ ServerHandler, handler::server::wrapper::Parameters, - model::{GetPromptResult, Prompt, PromptMessage, PromptMessageRole}, + model::{GetPromptResult, Prompt, PromptMessage, Role}, prompt, }; use schemars::JsonSchema; @@ -40,26 +40,20 @@ struct ComplexArgs { // Test basic prompt attribute generation #[prompt] async fn basic_prompt(_server: &TestServer) -> Vec { - vec![PromptMessage::new_text( - PromptMessageRole::Assistant, - "Basic response", - )] + vec![PromptMessage::new_text(Role::Assistant, "Basic response")] } // Test prompt with custom name #[prompt(name = "custom_name")] async fn named_prompt(_server: &TestServer) -> Vec { - vec![PromptMessage::new_text( - PromptMessageRole::Assistant, - "Named response", - )] + vec![PromptMessage::new_text(Role::Assistant, "Named response")] } // Test prompt with custom description #[prompt(description = "This is a custom description")] async fn described_prompt(_server: &TestServer) -> Vec { vec![PromptMessage::new_text( - PromptMessageRole::Assistant, + Role::Assistant, "Described response", )] } @@ -68,7 +62,7 @@ async fn described_prompt(_server: &TestServer) -> Vec { #[prompt(name = "full_custom", description = "Fully customized prompt")] async fn fully_custom_prompt(_server: &TestServer) -> Vec { vec![PromptMessage::new_text( - PromptMessageRole::Assistant, + Role::Assistant, "Fully custom response", )] } @@ -79,7 +73,7 @@ async fn fully_custom_prompt(_server: &TestServer) -> Vec { #[prompt] async fn doc_comment_prompt(_server: &TestServer) -> Vec { vec![PromptMessage::new_text( - PromptMessageRole::Assistant, + Role::Assistant, "Doc comment response", )] } @@ -89,7 +83,7 @@ async fn doc_comment_prompt(_server: &TestServer) -> Vec { #[prompt(description = "This overrides the doc comment")] async fn override_doc_prompt(_server: &TestServer) -> Vec { vec![PromptMessage::new_text( - PromptMessageRole::Assistant, + Role::Assistant, "Override response", )] } @@ -97,10 +91,7 @@ async fn override_doc_prompt(_server: &TestServer) -> Vec { // Test prompt with arguments #[prompt] async fn args_prompt(_server: &TestServer, _args: Parameters) -> Vec { - vec![PromptMessage::new_text( - PromptMessageRole::Assistant, - "Args response", - )] + vec![PromptMessage::new_text(Role::Assistant, "Args response")] } // Test prompt with complex arguments @@ -110,7 +101,7 @@ async fn complex_args_prompt( _args: Parameters, ) -> GetPromptResult { GetPromptResult::new(vec![PromptMessage::new_text( - PromptMessageRole::Assistant, + Role::Assistant, "Complex response", )]) .with_description("Complex args result") @@ -119,10 +110,7 @@ async fn complex_args_prompt( // Test sync prompt #[prompt] fn sync_prompt(_server: &TestServer) -> Vec { - vec![PromptMessage::new_text( - PromptMessageRole::Assistant, - "Sync response", - )] + vec![PromptMessage::new_text(Role::Assistant, "Sync response")] } #[test] @@ -275,10 +263,7 @@ impl ServerHandler for GenericServer {} async fn generic_prompt( _server: &GenericServer, ) -> Vec { - vec![PromptMessage::new_text( - PromptMessageRole::Assistant, - "Generic response", - )] + vec![PromptMessage::new_text(Role::Assistant, "Generic response")] } #[test] diff --git a/crates/rmcp/tests/test_prompt_macros.rs b/crates/rmcp/tests/test_prompt_macros.rs index b7c0c442f..e8d9d7dc0 100644 --- a/crates/rmcp/tests/test_prompt_macros.rs +++ b/crates/rmcp/tests/test_prompt_macros.rs @@ -7,8 +7,8 @@ use rmcp::{ ClientHandler, RoleServer, ServerHandler, ServiceExt, handler::server::{router::prompt::PromptRouter, wrapper::Parameters}, model::{ - ClientInfo, GetPromptRequestParams, GetPromptResult, ListPromptsResult, - PaginatedRequestParams, PromptMessage, PromptMessageRole, + ClientInfo, ContentBlock, GetPromptRequestParams, GetPromptResult, ListPromptsResult, + PaginatedRequestParams, PromptMessage, Role, }, prompt, prompt_handler, prompt_router, service::RequestContext, @@ -53,14 +53,14 @@ impl Server { pub async fn code_review(&self, params: Parameters) -> Vec { vec![ PromptMessage::new_text( - PromptMessageRole::User, + Role::User, format!( "Please review the {} code in: {}", params.0.language, params.0.file_path ), ), PromptMessage::new_text( - PromptMessageRole::Assistant, + Role::Assistant, "I'll review this code for best practices and potential issues.".to_string(), ), ] @@ -69,7 +69,7 @@ impl Server { #[prompt] async fn empty_param(&self) -> Vec { vec![PromptMessage::new_text( - PromptMessageRole::Assistant, + Role::Assistant, "This is a prompt with no parameters.".to_string(), )] } @@ -110,11 +110,11 @@ impl GenericServer { let context = self.data_service.get_context(); GetPromptResult::new(vec![ PromptMessage::new_text( - PromptMessageRole::User, + Role::User, "I need help with the current context.".to_string(), ), PromptMessage::new_text( - PromptMessageRole::Assistant, + Role::Assistant, format!( "Based on the context '{}', here's how I can help...", context @@ -141,8 +141,8 @@ async fn test_prompt_macros() { })) .await; assert_eq!(result.len(), 2); - assert_eq!(result[0].role, PromptMessageRole::User); - assert_eq!(result[1].role, PromptMessageRole::Assistant); + assert_eq!(result[0].role, Role::User); + assert_eq!(result[1].role, Role::Assistant); } #[tokio::test] @@ -166,8 +166,8 @@ async fn test_prompt_macros_with_generics() { assert!(result.description.is_some()); assert_eq!(result.messages.len(), 2); match &result.messages[1].content { - rmcp::model::PromptMessageContent::Text { text } => { - assert!(text.contains("mock context data")); + ContentBlock::Text(text_content) => { + assert!(text_content.text.contains("mock context data")); } _ => panic!("Expected text content"), } @@ -233,7 +233,7 @@ impl OptionalSchemaTester { #[prompt(description = "A prompt to test optional schema generation")] async fn test_optional(&self, _req: Parameters) -> Vec { vec![PromptMessage::new_text( - PromptMessageRole::Assistant, + Role::Assistant, "Testing optional fields".to_string(), )] } @@ -249,11 +249,8 @@ impl OptionalSchemaTester { None => "Received null count".to_string(), }; - GetPromptResult::new(vec![PromptMessage::new_text( - PromptMessageRole::Assistant, - message, - )]) - .with_description("Test result for optional i64") + GetPromptResult::new(vec![PromptMessage::new_text(Role::Assistant, message)]) + .with_description("Test result for optional i64") } } @@ -338,7 +335,7 @@ async fn test_optional_i64_field_with_null_input() -> anyhow::Result<()> { .await?; let result_text = match &result.messages.first().unwrap().content { - rmcp::model::PromptMessageContent::Text { text } => text.as_str(), + ContentBlock::Text(text_content) => text_content.text.as_str(), _ => panic!("Expected text content"), }; @@ -363,7 +360,7 @@ async fn test_optional_i64_field_with_null_input() -> anyhow::Result<()> { .await?; let some_result_text = match &some_result.messages.first().unwrap().content { - rmcp::model::PromptMessageContent::Text { text } => text.as_str(), + ContentBlock::Text(text_content) => text_content.text.as_str(), _ => panic!("Expected text content"), }; diff --git a/crates/rmcp/tests/test_prompt_routers.rs b/crates/rmcp/tests/test_prompt_routers.rs index 23674bd96..68b265db2 100644 --- a/crates/rmcp/tests/test_prompt_routers.rs +++ b/crates/rmcp/tests/test_prompt_routers.rs @@ -5,7 +5,7 @@ use futures::future::BoxFuture; use rmcp::{ ServerHandler, handler::server::wrapper::Parameters, - model::{GetPromptResult, PromptMessage, PromptMessageRole}, + model::{GetPromptResult, PromptMessage, Role}, }; #[derive(Debug, Default)] @@ -35,7 +35,7 @@ impl TestHandler { ) -> Vec { drop(fields); vec![PromptMessage::new_text( - PromptMessageRole::Assistant, + Role::Assistant, "Async method response", )] } @@ -47,7 +47,7 @@ impl TestHandler { ) -> Vec { drop(fields); vec![PromptMessage::new_text( - PromptMessageRole::Assistant, + Role::Assistant, "Sync method response", )] } @@ -57,7 +57,7 @@ impl TestHandler { async fn async_function(Parameters(Request { fields }): Parameters) -> Vec { drop(fields); vec![PromptMessage::new_text( - PromptMessageRole::Assistant, + Role::Assistant, "Async function response", )] } @@ -66,7 +66,7 @@ async fn async_function(Parameters(Request { fields }): Parameters) -> fn async_function2(_callee: &TestHandler) -> BoxFuture<'_, GetPromptResult> { Box::pin(async move { GetPromptResult::new(vec![PromptMessage::new_text( - PromptMessageRole::Assistant, + Role::Assistant, "Async function 2 response", )]) .with_description("Async function 2") diff --git a/crates/rmcp/tests/test_request_timeout_progress.rs b/crates/rmcp/tests/test_request_timeout_progress.rs index af62a466b..6eeac42e9 100644 --- a/crates/rmcp/tests/test_request_timeout_progress.rs +++ b/crates/rmcp/tests/test_request_timeout_progress.rs @@ -59,12 +59,11 @@ impl ProgressTimeoutServer { for step in 0..4 { tokio::time::sleep(Duration::from_millis(50)).await; let _ = client - .notify_progress(ProgressNotificationParam { - progress_token: progress_token.clone(), - progress: step as f64, - total: Some(4.0), - message: Some("working".into()), - }) + .notify_progress( + ProgressNotificationParam::new(progress_token.clone(), step as f64) + .with_total(4.0) + .with_message("working"), + ) .await; } @@ -79,12 +78,14 @@ impl ProgressTimeoutServer { for step in 0..4 { tokio::time::sleep(Duration::from_millis(50)).await; let _ = client - .notify_progress(ProgressNotificationParam { - progress_token: ProgressToken(NumberOrString::Number(999_999)), - progress: step as f64, - total: Some(4.0), - message: Some("unrelated".into()), - }) + .notify_progress( + ProgressNotificationParam::new( + ProgressToken(NumberOrString::Number(999_999)), + step as f64, + ) + .with_total(4.0) + .with_message("unrelated"), + ) .await; } diff --git a/crates/rmcp/tests/test_resource_link.rs b/crates/rmcp/tests/test_resource_link.rs index 685a645c4..7db5b06b7 100644 --- a/crates/rmcp/tests/test_resource_link.rs +++ b/crates/rmcp/tests/test_resource_link.rs @@ -1,14 +1,14 @@ -use rmcp::model::{CallToolResult, Content, RawResource}; +use rmcp::model::{CallToolResult, ContentBlock, Resource}; #[test] fn test_resource_link_in_tool_result() { // Test creating a tool result with resource links - let resource = RawResource::new("file:///test/file.txt", "test.txt"); + let resource = Resource::new("file:///test/file.txt", "test.txt"); // Create a tool result with a resource link let result = CallToolResult::success(vec![ - Content::text("Found a file"), - Content::resource_link(resource), + ContentBlock::text("Found a file"), + ContentBlock::resource_link(resource), ]); // Serialize to JSON to verify format @@ -42,12 +42,12 @@ fn test_resource_link_in_tool_result() { #[test] fn test_resource_link_with_full_metadata() { - let mut resource = RawResource::new("https://example.com/data.json", "API Data"); - resource.description = Some("JSON data from external API".to_string()); - resource.mime_type = Some("application/json".to_string()); - resource.size = Some(1024); + let resource = Resource::new("https://example.com/data.json", "API Data") + .with_description("JSON data from external API") + .with_mime_type("application/json") + .with_size(1024); - let result = CallToolResult::success(vec![Content::resource_link(resource)]); + let result = CallToolResult::success(vec![ContentBlock::resource_link(resource)]); let json = serde_json::to_string(&result).unwrap(); let deserialized: CallToolResult = serde_json::from_str(&json).unwrap(); @@ -71,12 +71,12 @@ fn test_resource_link_with_full_metadata() { #[test] fn test_mixed_content_types() { // Test that resource links can be mixed with other content types - let resource = RawResource::new("file:///doc.pdf", "Document"); + let resource = Resource::new("file:///doc.pdf", "Document"); let result = CallToolResult::success(vec![ - Content::text("Processing complete"), - Content::resource_link(resource), - Content::embedded_text("memo://result", "Analysis results here"), + ContentBlock::text("Processing complete"), + ContentBlock::resource_link(resource), + ContentBlock::embedded_text("memo://result", "Analysis results here"), ]); assert_eq!(result.content.len(), 3); diff --git a/crates/rmcp/tests/test_resource_link_integration.rs b/crates/rmcp/tests/test_resource_link_integration.rs index 7507d71e0..2cc687285 100644 --- a/crates/rmcp/tests/test_resource_link_integration.rs +++ b/crates/rmcp/tests/test_resource_link_integration.rs @@ -1,27 +1,21 @@ /// Integration tests for resource_link support in both tools and prompts -use rmcp::model::{ - AnnotateAble, CallToolResult, Content, PromptMessage, PromptMessageContent, PromptMessageRole, - RawResource, Resource, -}; +use rmcp::model::{CallToolResult, ContentBlock, PromptMessage, Resource, Role}; #[test] fn test_tool_and_prompt_resource_link_compatibility() { - // Create a resource that can be used in both tools and prompts - let resource = RawResource::new("file:///shared/data.json", "Shared Data"); - let resource_annotated: Resource = resource.clone().no_annotation(); + let resource = Resource::new("file:///shared/data.json", "Shared Data"); // Test 1: Tool returning a resource link let tool_result = CallToolResult::success(vec![ - Content::text("Found shared data"), - Content::resource_link(resource.clone()), + ContentBlock::text("Found shared data"), + ContentBlock::resource_link(resource.clone()), ]); let tool_json = serde_json::to_string(&tool_result).unwrap(); assert!(tool_json.contains("\"type\":\"resource_link\"")); // Test 2: Prompt returning a resource link - let prompt_message = - PromptMessage::new_resource_link(PromptMessageRole::Assistant, resource_annotated.clone()); + let prompt_message = PromptMessage::new_resource_link(Role::Assistant, resource.clone()); let prompt_json = serde_json::to_string(&prompt_message).unwrap(); assert!(prompt_json.contains("\"type\":\"resource_link\"")); @@ -30,11 +24,9 @@ fn test_tool_and_prompt_resource_link_compatibility() { let tool_content = &tool_result.content[1]; let prompt_content = &prompt_message.content; - // Extract just the resource link parts let tool_resource_json = serde_json::to_value(tool_content).unwrap(); let prompt_resource_json = serde_json::to_value(prompt_content).unwrap(); - // Both should have the same structure assert_eq!( tool_resource_json.get("type").unwrap(), prompt_resource_json.get("type").unwrap() @@ -51,16 +43,13 @@ fn test_tool_and_prompt_resource_link_compatibility() { #[test] fn test_resource_link_roundtrip() { - // Test that resource links can be serialized and deserialized correctly - // in both tool results and prompt messages - - let mut resource = RawResource::new("https://api.example.com/resource", "API Resource"); - resource.description = Some("External API resource".to_string()); - resource.mime_type = Some("application/json".to_string()); - resource.size = Some(2048); + let resource = Resource::new("https://api.example.com/resource", "API Resource") + .with_description("External API resource") + .with_mime_type("application/json") + .with_size(2048); // Test with tool result - let tool_result = CallToolResult::success(vec![Content::resource_link(resource.clone())]); + let tool_result = CallToolResult::success(vec![ContentBlock::resource_link(resource.clone())]); let tool_json = serde_json::to_string(&tool_result).unwrap(); let tool_deserialized: CallToolResult = serde_json::from_str(&tool_json).unwrap(); @@ -82,15 +71,12 @@ fn test_resource_link_roundtrip() { } // Test with prompt message - let prompt_message = PromptMessage::new( - PromptMessageRole::User, - PromptMessageContent::resource_link(resource.no_annotation()), - ); + let prompt_message = PromptMessage::new(Role::User, ContentBlock::resource_link(resource)); let prompt_json = serde_json::to_string(&prompt_message).unwrap(); let prompt_deserialized: PromptMessage = serde_json::from_str(&prompt_json).unwrap(); - if let PromptMessageContent::ResourceLink { link } = prompt_deserialized.content { + if let ContentBlock::ResourceLink(link) = &prompt_deserialized.content { assert_eq!(link.uri, "https://api.example.com/resource"); assert_eq!(link.name, "API Resource"); assert_eq!(link.description, Some("External API resource".to_string())); @@ -103,18 +89,15 @@ fn test_resource_link_roundtrip() { #[test] fn test_mixed_content_in_prompts_and_tools() { - // Test that resource links can be mixed with other content types - // in both prompts and tools - - let resource1 = RawResource::new("file:///doc1.md", "Document 1"); - let resource2 = RawResource::new("file:///doc2.md", "Document 2"); + let resource1 = Resource::new("file:///doc1.md", "Document 1"); + let resource2 = Resource::new("file:///doc2.md", "Document 2"); // Tool with mixed content let tool_result = CallToolResult::success(vec![ - Content::text("Processing complete. Found documents:"), - Content::resource_link(resource1.clone()), - Content::resource_link(resource2.clone()), - Content::embedded_text("summary://result", "Both documents processed successfully"), + ContentBlock::text("Processing complete. Found documents:"), + ContentBlock::resource_link(resource1), + ContentBlock::resource_link(resource2), + ContentBlock::embedded_text("summary://result", "Both documents processed successfully"), ]); assert_eq!(tool_result.content.len(), 4); diff --git a/crates/rmcp/tests/test_sampling.rs b/crates/rmcp/tests/test_sampling.rs index 74e904ff5..283826897 100644 --- a/crates/rmcp/tests/test_sampling.rs +++ b/crates/rmcp/tests/test_sampling.rs @@ -345,7 +345,7 @@ async fn test_tool_use_content_serialization() -> Result<()> { async fn test_tool_result_content_serialization() -> Result<()> { let tool_result = ToolResultContent::new( "call_123", - vec![Content::text( + vec![ContentBlock::text( "The weather in San Francisco is 72°F and sunny.", )], ); @@ -386,7 +386,7 @@ async fn test_sampling_message_with_tool_use() -> Result<()> { #[tokio::test] async fn test_sampling_message_with_tool_result() -> Result<()> { let message = - SamplingMessage::user_tool_result("call_123", vec![Content::text("72°F and sunny")]); + SamplingMessage::user_tool_result("call_123", vec![ContentBlock::text("72°F and sunny")]); let json = serde_json::to_string(&message)?; let deserialized: SamplingMessage = serde_json::from_str(&json)?; @@ -431,10 +431,8 @@ async fn test_create_message_result_tool_use_stop_reason() -> Result<()> { #[tokio::test] async fn test_sampling_capability() -> Result<()> { - let cap = SamplingCapability { - tools: Some(JsonObject::default()), - context: None, - }; + let mut cap = SamplingCapability::default(); + cap.tools = Some(JsonObject::default()); let json = serde_json::to_string(&cap)?; let deserialized: SamplingCapability = serde_json::from_str(&json)?; @@ -507,16 +505,19 @@ async fn test_backward_compat_sampling_capability_empty_object() -> Result<()> { async fn test_content_to_sampling_message_content_conversion() -> Result<()> { use std::convert::TryInto; - let content = Content::text("Hello"); - let sampling_content: SamplingMessageContent = + let content = ContentBlock::text("Hello"); + let sampling_content: SamplingMessageContentBlock = content.try_into().map_err(|e: &str| anyhow::anyhow!(e))?; assert!(sampling_content.as_text().is_some()); assert_eq!(sampling_content.as_text().unwrap().text, "Hello"); - let content = Content::image("base64data", "image/png"); - let sampling_content: SamplingMessageContent = + let content = ContentBlock::image("base64data", "image/png"); + let sampling_content: SamplingMessageContentBlock = content.try_into().map_err(|e: &str| anyhow::anyhow!(e))?; - assert!(matches!(sampling_content, SamplingMessageContent::Image(_))); + assert!(matches!( + sampling_content, + SamplingMessageContentBlock::Image(_) + )); Ok(()) } @@ -525,8 +526,8 @@ async fn test_content_to_sampling_message_content_conversion() -> Result<()> { async fn test_content_to_sampling_content_conversion() -> Result<()> { use std::convert::TryInto; - let content = Content::text("Hello"); - let sampling_content: SamplingContent = + let content = ContentBlock::text("Hello"); + let sampling_content: SamplingContent = content.try_into().map_err(|e: &str| anyhow::anyhow!(e))?; assert_eq!(sampling_content.len(), 1); assert!(sampling_content.first().unwrap().as_text().is_some()); @@ -540,14 +541,14 @@ async fn test_content_conversion_unsupported_variants() { use rmcp::model::ResourceContents; - let resource_content = Content::resource(ResourceContents::TextResourceContents { + let resource_content = ContentBlock::resource(ResourceContents::TextResourceContents { uri: "file:///test.txt".to_string(), mime_type: Some("text/plain".to_string()), text: "test".to_string(), meta: None, }); - let result: Result = resource_content.try_into(); + let result: Result = resource_content.try_into(); assert!(result.is_err()); assert_eq!( result.unwrap_err(), @@ -560,7 +561,7 @@ async fn test_validate_rejects_tool_use_in_user_message() { let params = CreateMessageRequestParams::new( vec![SamplingMessage::new( Role::User, - SamplingMessageContent::tool_use("call_1", "some_tool", Default::default()), + SamplingMessageContentBlock::tool_use("call_1", "some_tool", Default::default()), )], 100, ); @@ -577,7 +578,7 @@ async fn test_validate_rejects_tool_result_in_assistant_message() { let params = CreateMessageRequestParams::new( vec![SamplingMessage::new( Role::Assistant, - SamplingMessageContent::tool_result("call_1", vec![Content::text("result")]), + SamplingMessageContentBlock::tool_result("call_1", vec![ContentBlock::text("result")]), )], 100, ); @@ -595,8 +596,11 @@ async fn test_validate_rejects_mixed_content_with_tool_result() { vec![SamplingMessage::new_multiple( Role::User, vec![ - SamplingMessageContent::tool_result("call_1", vec![Content::text("result")]), - SamplingMessageContent::text("some extra text"), + SamplingMessageContentBlock::tool_result( + "call_1", + vec![ContentBlock::text("result")], + ), + SamplingMessageContentBlock::text("some extra text"), ], )], 100, @@ -631,7 +635,10 @@ async fn test_validate_rejects_tool_result_without_matching_use() { let params = CreateMessageRequestParams::new( vec![ SamplingMessage::user_text("Hello"), - SamplingMessage::user_tool_result("nonexistent_call", vec![Content::text("result")]), + SamplingMessage::user_tool_result( + "nonexistent_call", + vec![ContentBlock::text("result")], + ), ], 100, ); @@ -656,7 +663,7 @@ async fn test_validate_accepts_valid_tool_conversation() { .unwrap() .clone(), ), - SamplingMessage::user_tool_result("call_1", vec![Content::text("72°F and sunny")]), + SamplingMessage::user_tool_result("call_1", vec![ContentBlock::text("72°F and sunny")]), SamplingMessage::assistant_text("It's 72°F and sunny in SF."), ], 100, diff --git a/crates/rmcp/tests/test_sse_concurrent_streams.rs b/crates/rmcp/tests/test_sse_concurrent_streams.rs index 37bbfd721..e1e885282 100644 --- a/crates/rmcp/tests/test_sse_concurrent_streams.rs +++ b/crates/rmcp/tests/test_sse_concurrent_streams.rs @@ -48,8 +48,10 @@ impl ServerHandler for TestServer { fn get_info(&self) -> ServerInfo { ServerInfo::new( ServerCapabilities::builder() - .enable_tools_with(ToolsCapability { - list_changed: Some(true), + .enable_tools_with({ + let mut tools = ToolsCapability::default(); + tools.list_changed = Some(true); + tools }) .build(), ) diff --git a/crates/rmcp/tests/test_structured_output.rs b/crates/rmcp/tests/test_structured_output.rs index adbdfec5e..bb0d5e029 100644 --- a/crates/rmcp/tests/test_structured_output.rs +++ b/crates/rmcp/tests/test_structured_output.rs @@ -3,7 +3,7 @@ use rmcp::{ Json, ServerHandler, handler::server::{router::tool::ToolRouter, tool::IntoCallToolResult, wrapper::Parameters}, - model::{CallToolResult, Content, ServerResult, Tool}, + model::{CallToolResult, ContentBlock, ServerResult, Tool}, tool, tool_handler, tool_router, }; use schemars::JsonSchema; @@ -192,7 +192,8 @@ async fn test_mutual_exclusivity_validation() { message: "Hello".into(), }; // Test that content and structured_content can both be passed separately - let content_result = CallToolResult::success(vec![Content::json(response.clone()).unwrap()]); + let content_result = + CallToolResult::success(vec![ContentBlock::json(response.clone()).unwrap()]); let structured_result = CallToolResult::structured(json!({"message": "Hello"})); // Verify the validation diff --git a/crates/rmcp/tests/test_task_support_validation.rs b/crates/rmcp/tests/test_task_support_validation.rs index c0a65a9e0..773f759f9 100644 --- a/crates/rmcp/tests/test_task_support_validation.rs +++ b/crates/rmcp/tests/test_task_support_validation.rs @@ -11,7 +11,7 @@ use rmcp::{ ClientHandler, ServerHandler, ServiceError, ServiceExt, handler::server::router::tool::ToolRouter, - model::{CallToolRequestParams, ClientInfo, ErrorCode, JsonObject}, + model::{CallToolRequestParams, ClientInfo, ErrorCode, TaskMetadata}, tool, tool_handler, tool_router, }; @@ -75,8 +75,8 @@ impl ClientHandler for DummyClientHandler { } /// Helper to create a task object for tool calls -fn make_task() -> JsonObject { - serde_json::Map::new() +fn make_task() -> TaskMetadata { + TaskMetadata::new() } #[tokio::test] @@ -203,7 +203,7 @@ async fn test_forbidden_task_tool_without_task_succeeds() -> anyhow::Result<()> let text = result .content .first() - .and_then(|c| c.raw.as_text()) + .and_then(|c| c.as_text()) .map(|t| t.text.as_str()) .unwrap_or(""); assert_eq!(text, "forbidden task executed"); @@ -239,7 +239,7 @@ async fn test_optional_task_tool_without_task_succeeds() -> anyhow::Result<()> { let text = result .content .first() - .and_then(|c| c.raw.as_text()) + .and_then(|c| c.as_text()) .map(|t| t.text.as_str()) .unwrap_or(""); assert_eq!(text, "optional task executed"); diff --git a/crates/rmcp/tests/test_tool_macros.rs b/crates/rmcp/tests/test_tool_macros.rs index 902b314f1..ed2e697a2 100644 --- a/crates/rmcp/tests/test_tool_macros.rs +++ b/crates/rmcp/tests/test_tool_macros.rs @@ -325,7 +325,7 @@ async fn test_optional_i64_field_with_null_input() -> anyhow::Result<()> { let result_text = result .content .first() - .and_then(|content| content.raw.as_text()) + .and_then(|content| content.as_text()) .map(|text| text.text.as_str()) .expect("Expected text content"); @@ -352,7 +352,7 @@ async fn test_optional_i64_field_with_null_input() -> anyhow::Result<()> { let some_result_text = some_result .content .first() - .and_then(|content| content.raw.as_text()) + .and_then(|content| content.as_text()) .map(|text| text.text.as_str()) .expect("Expected text content"); @@ -438,7 +438,7 @@ async fn test_minimal_server_tool_call() -> anyhow::Result<()> { let text = result .content .first() - .and_then(|c| c.raw.as_text()) + .and_then(|c| c.as_text()) .map(|t| t.text.as_str()) .expect("Expected text content"); @@ -496,7 +496,7 @@ async fn test_tool_router_server_handler_flag_end_to_end_tool_call() -> anyhow:: let text = result .content .first() - .and_then(|c| c.raw.as_text()) + .and_then(|c| c.as_text()) .map(|t| t.text.as_str()) .expect("Expected text content"); diff --git a/crates/rmcp/tests/test_tool_result_meta.rs b/crates/rmcp/tests/test_tool_result_meta.rs index f64d8e3f1..a1d3d3af3 100644 --- a/crates/rmcp/tests/test_tool_result_meta.rs +++ b/crates/rmcp/tests/test_tool_result_meta.rs @@ -1,9 +1,9 @@ -use rmcp::model::{CallToolResult, Content, Meta}; +use rmcp::model::{CallToolResult, ContentBlock, Meta}; use serde_json::{Value, json}; #[test] fn serialize_tool_result_with_meta() { - let content = vec![Content::text("ok")]; + let content = vec![ContentBlock::text("ok")]; let mut meta = Meta::new(); meta.insert("foo".to_string(), json!("bar")); let result = CallToolResult::success(content).with_meta(Some(meta)); @@ -33,7 +33,7 @@ fn deserialize_tool_result_with_meta() { #[test] fn serialize_tool_result_without_meta_omits_field() { - let result = CallToolResult::success(vec![Content::text("no meta")]); + let result = CallToolResult::success(vec![ContentBlock::text("no meta")]); let v = serde_json::to_value(&result).unwrap(); // Ensure _meta is omitted assert!(v.get("_meta").is_none()); diff --git a/examples/clients/src/task_stdio.rs b/examples/clients/src/task_stdio.rs index 472a9c370..b384ff29a 100644 --- a/examples/clients/src/task_stdio.rs +++ b/examples/clients/src/task_stdio.rs @@ -11,8 +11,8 @@ use anyhow::{Result, anyhow}; use rmcp::{ ServiceExt, model::{ - CallToolRequestParams, CallToolResult, ClientRequest, GetTaskInfoParams, - GetTaskResultParams, JsonObject, Request, ServerResult, TaskStatus, + CallToolRequestParams, CallToolResult, ClientRequest, GetTaskParams, GetTaskPayloadParams, + Request, ServerResult, TaskMetadata, TaskStatus, }, object, transport::{ConfigureCommandExt, TokioChildProcess}, @@ -60,7 +60,7 @@ async fn main() -> Result<()> { .send_request(ClientRequest::CallToolRequest(Request::new( CallToolRequestParams::new("slow_sum") .with_arguments(object!({ "a": 40, "b": 2 })) - .with_task(JsonObject::new()), + .with_task(TaskMetadata::new()), ))) .await?; let ServerResult::CreateTaskResult(create) = create else { @@ -77,11 +77,8 @@ async fn main() -> Result<()> { tokio::time::sleep(std::time::Duration::from_millis(250)).await; let info = client - .send_request(ClientRequest::GetTaskInfoRequest(Request::new( - GetTaskInfoParams { - meta: None, - task_id: task_id.clone(), - }, + .send_request(ClientRequest::GetTaskRequest(Request::new( + GetTaskParams::new(task_id.clone()), ))) .await?; let ServerResult::GetTaskResult(info) = info else { @@ -108,11 +105,8 @@ async fn main() -> Result<()> { // here. (For a non-tool task the same value would surface as // `ServerResult::CustomResult` and need manual `serde_json::from_value`.) let payload = client - .send_request(ClientRequest::GetTaskResultRequest(Request::new( - GetTaskResultParams { - meta: None, - task_id: task_id.clone(), - }, + .send_request(ClientRequest::GetTaskPayloadRequest(Request::new( + GetTaskPayloadParams::new(task_id.clone()), ))) .await?; let call_result: CallToolResult = match payload { diff --git a/examples/servers/src/common/counter.rs b/examples/servers/src/common/counter.rs index d78acd59f..f618fee0d 100644 --- a/examples/servers/src/common/counter.rs +++ b/examples/servers/src/common/counter.rs @@ -94,14 +94,14 @@ impl Counter { } fn _create_resource_text(&self, uri: &str, name: &str) -> Resource { - RawResource::new(uri, name.to_string()).no_annotation() + Resource::new(uri, name.to_string()) } #[tool(description = "Increment the counter by 1")] async fn increment(&self) -> Result { let mut counter = self.counter.lock().await; *counter += 1; - Ok(CallToolResult::success(vec![Content::text( + Ok(CallToolResult::success(vec![ContentBlock::text( counter.to_string(), )])) } @@ -110,7 +110,7 @@ impl Counter { async fn decrement(&self) -> Result { let mut counter = self.counter.lock().await; *counter -= 1; - Ok(CallToolResult::success(vec![Content::text( + Ok(CallToolResult::success(vec![ContentBlock::text( counter.to_string(), )])) } @@ -118,7 +118,7 @@ impl Counter { #[tool(description = "Get the current counter value")] async fn get_value(&self) -> Result { let counter = self.counter.lock().await; - Ok(CallToolResult::success(vec![Content::text( + Ok(CallToolResult::success(vec![ContentBlock::text( counter.to_string(), )])) } @@ -129,19 +129,19 @@ impl Counter { )] async fn long_task(&self) -> Result { tokio::time::sleep(std::time::Duration::from_secs(10)).await; - Ok(CallToolResult::success(vec![Content::text( + Ok(CallToolResult::success(vec![ContentBlock::text( "Long task completed", )])) } #[tool(description = "Say hello to the client")] fn say_hello(&self) -> Result { - Ok(CallToolResult::success(vec![Content::text("hello")])) + Ok(CallToolResult::success(vec![ContentBlock::text("hello")])) } #[tool(description = "Repeat what you say")] fn echo(&self, Parameters(object): Parameters) -> Result { - Ok(CallToolResult::success(vec![Content::text( + Ok(CallToolResult::success(vec![ContentBlock::text( serde_json::Value::Object(object).to_string(), )])) } @@ -151,7 +151,7 @@ impl Counter { &self, Parameters(StructRequest { a, b }): Parameters, ) -> Result { - Ok(CallToolResult::success(vec![Content::text( + Ok(CallToolResult::success(vec![ContentBlock::text( (a + b).to_string(), )])) } @@ -166,8 +166,8 @@ impl Counter { .map(|v| v.to_str().unwrap_or("(non-ascii)").to_owned()); match session_id { - Some(id) => Ok(CallToolResult::success(vec![Content::text(id)])), - None => Ok(CallToolResult::success(vec![Content::text( + Some(id) => Ok(CallToolResult::success(vec![ContentBlock::text(id)])), + None => Ok(CallToolResult::success(vec![ContentBlock::text( "no session (not running over streamable HTTP?)", )])), } @@ -190,10 +190,7 @@ impl Counter { "This is an example prompt with your message here: '{}'", args.message ); - Ok(vec![PromptMessage::new_text( - PromptMessageRole::User, - prompt, - )]) + Ok(vec![PromptMessage::new_text(Role::User, prompt)]) } /// Analyze the current counter value and suggest next steps @@ -209,11 +206,11 @@ impl Counter { let messages = vec![ PromptMessage::new_text( - PromptMessageRole::Assistant, + Role::Assistant, "I'll analyze the counter situation and suggest the best approach.", ), PromptMessage::new_text( - PromptMessageRole::User, + Role::User, format!( "Current counter value: {}\nGoal value: {}\nDifference: {}\nStrategy preference: {}\n\nPlease analyze the situation and suggest the best approach to reach the goal.", current_value, args.goal, difference, strategy @@ -406,12 +403,7 @@ mod tests { }); let client_service = client.serve(client_transport).await?; - let mut task_meta = serde_json::Map::new(); - task_meta.insert( - "source".into(), - serde_json::Value::String("integration-test".into()), - ); - let params = CallToolRequestParams::new("long_task").with_task(task_meta); + let params = CallToolRequestParams::new("long_task").with_task(TaskMetadata::new()); let response = client_service .send_request(ClientRequest::CallToolRequest(Request::new(params.clone()))) .await?; diff --git a/examples/servers/src/common/progress_demo.rs b/examples/servers/src/common/progress_demo.rs index ba6fa7d13..253dbc28e 100644 --- a/examples/servers/src/common/progress_demo.rs +++ b/examples/servers/src/common/progress_demo.rs @@ -97,12 +97,12 @@ impl ProgressDemo { let chunk_str = String::from_utf8_lossy(&chunk); counter += 1; // create progress notification param - let progress_param = ProgressNotificationParam { - progress_token: ProgressToken(progress_token.clone()), - progress: counter as f64, - total: Some(5.0), - message: Some(chunk_str.to_string()), - }; + let progress_param = ProgressNotificationParam::new( + ProgressToken(progress_token.clone()), + counter as f64, + ) + .with_total(5.0) + .with_message(chunk_str.to_string()); match ctx.peer.notify_progress(progress_param).await { Ok(_) => { @@ -122,7 +122,7 @@ impl ProgressDemo { tokio::time::sleep(std::time::Duration::from_secs(1)).await; } - Ok(CallToolResult::success(vec![Content::text(format!( + Ok(CallToolResult::success(vec![ContentBlock::text(format!( "Processed {} records successfully", counter ))])) diff --git a/examples/servers/src/common/task_demo.rs b/examples/servers/src/common/task_demo.rs index 27bff2195..275047a55 100644 --- a/examples/servers/src/common/task_demo.rs +++ b/examples/servers/src/common/task_demo.rs @@ -16,7 +16,7 @@ use std::sync::Arc; use rmcp::{ ErrorData as McpError, ServerHandler, handler::server::{router::tool::ToolRouter, wrapper::Parameters}, - model::{CallToolResult, Content}, + model::{CallToolResult, ContentBlock}, schemars, task_handler, task_manager::OperationProcessor, tool, tool_handler, tool_router, @@ -70,7 +70,7 @@ impl TaskDemo { Parameters(SumArgs { a, b }): Parameters, ) -> Result { tokio::time::sleep(std::time::Duration::from_secs(2)).await; - Ok(CallToolResult::success(vec![Content::text( + Ok(CallToolResult::success(vec![ContentBlock::text( (a + b).to_string(), )])) } @@ -81,7 +81,7 @@ impl TaskDemo { &self, Parameters(EchoArgs { message }): Parameters, ) -> Result { - Ok(CallToolResult::success(vec![Content::text(message)])) + Ok(CallToolResult::success(vec![ContentBlock::text(message)])) } } diff --git a/examples/servers/src/completion_stdio.rs b/examples/servers/src/completion_stdio.rs index 7caa8e8c3..812ed31a8 100644 --- a/examples/servers/src/completion_stdio.rs +++ b/examples/servers/src/completion_stdio.rs @@ -203,11 +203,11 @@ impl SqlQueryServer { let messages = if args.operation.is_empty() { vec![ PromptMessage::new_text( - PromptMessageRole::User, + Role::User, "I need help building a SQL query. Where should I start?", ), PromptMessage::new_text( - PromptMessageRole::Assistant, + Role::Assistant, "I'll help you build a SQL query step by step. First, what type of operation do you want to perform? \ Choose from: SELECT (to read data), INSERT (to add data), UPDATE (to modify data), or DELETE (to remove data).", ), @@ -215,11 +215,11 @@ impl SqlQueryServer { } else if args.table.is_empty() { vec![ PromptMessage::new_text( - PromptMessageRole::User, + Role::User, format!("I want to {} data. What's next?", args.operation), ), PromptMessage::new_text( - PromptMessageRole::Assistant, + Role::Assistant, format!( "Great! For a {} operation, I need to know which table you want to work with. \ What's the name of your database table?", @@ -277,11 +277,11 @@ impl SqlQueryServer { vec![ PromptMessage::new_text( - PromptMessageRole::User, + Role::User, "Generate the SQL query based on my parameters and explain what it does.", ), PromptMessage::new_text( - PromptMessageRole::Assistant, + Role::Assistant, format!( "Here's your SQL query:\n\n```sql\n{}\n```\n\nThis query will {} the {} table.", query, @@ -405,11 +405,8 @@ impl ServerHandler for SqlQueryServer { let suggestions = self.fuzzy_match(&request.argument.value, &candidates); - let completion = CompletionInfo { - values: suggestions, - total: None, - has_more: Some(false), - }; + let completion = CompletionInfo::with_pagination(suggestions, None, false) + .map_err(|e| McpError::internal_error(e, None))?; Ok(CompleteResult::new(completion)) } diff --git a/examples/servers/src/elicitation_enum_inference.rs b/examples/servers/src/elicitation_enum_inference.rs index 328fb8c88..bed5c1db6 100644 --- a/examples/servers/src/elicitation_enum_inference.rs +++ b/examples/servers/src/elicitation_enum_inference.rs @@ -114,7 +114,7 @@ impl ElicitationEnumFormServer { #[tool(description = "Get current enum selection form")] async fn get_enum_form(&self) -> Result { let guard = self.selection.lock().await; - Ok(CallToolResult::success(vec![Content::text(format!( + Ok(CallToolResult::success(vec![ContentBlock::text(format!( "{}", *guard ))])) @@ -133,13 +133,13 @@ impl ElicitationEnumFormServer { Ok(Some(form)) => { let mut guard = self.selection.lock().await; *guard = form; - Ok(CallToolResult::success(vec![Content::text(format!( + Ok(CallToolResult::success(vec![ContentBlock::text(format!( "Updated Selection:\n{}", *guard ))])) } Ok(None) => { - return Ok(CallToolResult::success(vec![Content::text( + return Ok(CallToolResult::success(vec![ContentBlock::text( "Elicitation cancelled by user.", )])); } diff --git a/examples/servers/src/elicitation_stdio.rs b/examples/servers/src/elicitation_stdio.rs index 3bf38056e..e465e4b4d 100644 --- a/examples/servers/src/elicitation_stdio.rs +++ b/examples/servers/src/elicitation_stdio.rs @@ -94,7 +94,7 @@ impl ElicitationServer { } }; - Ok(CallToolResult::success(vec![Content::text(format!( + Ok(CallToolResult::success(vec![ContentBlock::text(format!( "{} {}!", request.greeting, user_name ))])) @@ -103,7 +103,7 @@ impl ElicitationServer { #[tool(description = "Reset stored user name")] async fn reset_name(&self) -> Result { *self.user_name.lock().await = None; - Ok(CallToolResult::success(vec![Content::text( + Ok(CallToolResult::success(vec![ContentBlock::text( "User name reset. Next greeting will ask for name again.".to_string(), )])) } @@ -133,20 +133,23 @@ impl ElicitationServer { // Mock notifying completion let _ = context .peer - .notify_url_elicitation_completed(ElicitationResponseNotificationParam { - elicitation_id: "elicit_123".to_string(), - }) + .notify_url_elicitation_completed(ElicitationResponseNotificationParam::new( + "elicit_123", + )) .await; - Ok(CallToolResult::success(vec![Content::text( + Ok(CallToolResult::success(vec![ContentBlock::text( "Elicitation via URL successful".to_string(), )])) } - ElicitationAction::Cancel => Ok(CallToolResult::success(vec![Content::text( + ElicitationAction::Cancel => Ok(CallToolResult::success(vec![ContentBlock::text( "Elicitation via URL cancelled by user".to_string(), )])), - ElicitationAction::Decline => Ok(CallToolResult::error(vec![Content::text( + ElicitationAction::Decline => Ok(CallToolResult::error(vec![ContentBlock::text( "Elicitation via URL declined by user".to_string(), )])), + _ => Ok(CallToolResult::error(vec![ContentBlock::text( + "Unknown elicitation action".to_string(), + )])), } } } diff --git a/examples/servers/src/prompt_stdio.rs b/examples/servers/src/prompt_stdio.rs index 812ce0e1b..7b6e28532 100644 --- a/examples/servers/src/prompt_stdio.rs +++ b/examples/servers/src/prompt_stdio.rs @@ -121,12 +121,9 @@ impl PromptServer { )] async fn greeting(&self) -> Vec { vec![ + PromptMessage::new_text(Role::User, "Hello! I'd like to start our conversation."), PromptMessage::new_text( - PromptMessageRole::User, - "Hello! I'd like to start our conversation.", - ), - PromptMessage::new_text( - PromptMessageRole::Assistant, + Role::Assistant, "Hello! I'm here to help. What would you like to discuss today?", ), ] @@ -148,14 +145,14 @@ impl PromptServer { let messages = vec![ PromptMessage::new_text( - PromptMessageRole::Assistant, + Role::Assistant, format!( "You are an expert {} code reviewer. The user's expertise level is {}.", args.language, prefs.expertise_level ), ), PromptMessage::new_text( - PromptMessageRole::User, + Role::User, format!( "Please review the {} code at '{}'. Focus on: {}", args.language, @@ -164,7 +161,7 @@ impl PromptServer { ), ), PromptMessage::new_text( - PromptMessageRole::Assistant, + Role::Assistant, format!( "I'll review your {} code focusing on {}. Let me analyze the code at '{}'...", args.language, @@ -203,14 +200,14 @@ impl PromptServer { Ok(vec![ PromptMessage::new_text( - PromptMessageRole::User, + Role::User, format!( "I have {} data that needs {} analysis. Context: {}", args.data_type, args.analysis_type, context ), ), PromptMessage::new_text( - PromptMessageRole::Assistant, + Role::Assistant, format!( "I'll help you analyze your {} data using {} techniques. Based on your context, \ I'll focus on providing actionable insights.", @@ -233,7 +230,7 @@ impl PromptServer { let mut messages = vec![ PromptMessage::new_text( - PromptMessageRole::Assistant, + Role::Assistant, format!( "You are a writing assistant helping create {} content for {}. \ Use a {} tone.", @@ -241,7 +238,7 @@ impl PromptServer { ), ), PromptMessage::new_text( - PromptMessageRole::User, + Role::User, format!( "I need help writing {} for {}. Key points to cover: {}", args.content_type, @@ -250,7 +247,7 @@ impl PromptServer { ), ), PromptMessage::new_text( - PromptMessageRole::Assistant, + Role::Assistant, "I'll help you create that content. Let me structure it based on your key points.", ), ]; @@ -258,11 +255,11 @@ impl PromptServer { // Add a message for each key point for (i, point) in args.key_points.iter().enumerate() { messages.push(PromptMessage::new_text( - PromptMessageRole::User, + Role::User, format!("For point {}: {}, what would you suggest?", i + 1, point), )); messages.push(PromptMessage::new_text( - PromptMessageRole::Assistant, + Role::Assistant, format!("For '{}', I recommend...", point), )); } @@ -291,14 +288,14 @@ impl PromptServer { let mut messages = vec![ PromptMessage::new_text( - PromptMessageRole::Assistant, + Role::Assistant, format!( "You are a debugging expert for {}. Help diagnose and fix issues.", args.stack.join(", ") ), ), PromptMessage::new_text( - PromptMessageRole::User, + Role::User, format!( "I'm encountering this error: {}\nStack: {}", args.error_message, @@ -311,18 +308,18 @@ impl PromptServer { if let Some(tried) = args.tried_solutions { if !tried.is_empty() { messages.push(PromptMessage::new_text( - PromptMessageRole::User, + Role::User, format!("I've already tried: {}", tried.join(", ")), )); messages.push(PromptMessage::new_text( - PromptMessageRole::Assistant, + Role::Assistant, "I see you've already attempted some solutions. Let me suggest different approaches.", )); } } messages.push(PromptMessage::new_text( - PromptMessageRole::Assistant, + Role::Assistant, "Let's debug this systematically. First, let me understand the error context better.", )); @@ -343,18 +340,18 @@ impl PromptServer { Ok(vec![ PromptMessage::new_text( - PromptMessageRole::Assistant, + Role::Assistant, format!( "Create a learning path for someone at {} level who prefers {} language explanations.", prefs.expertise_level, prefs.preferred_language ), ), PromptMessage::new_text( - PromptMessageRole::User, + Role::User, "What should I learn next to improve my programming skills?", ), PromptMessage::new_text( - PromptMessageRole::Assistant, + Role::Assistant, format!( "Based on your {} expertise level, I recommend the following learning path...", prefs.expertise_level diff --git a/examples/servers/src/sampling_stdio.rs b/examples/servers/src/sampling_stdio.rs index 9c1d21d6d..f75c445f6 100644 --- a/examples/servers/src/sampling_stdio.rs +++ b/examples/servers/src/sampling_stdio.rs @@ -69,7 +69,7 @@ impl ServerHandler for SamplingDemoServer { ) })?; tracing::debug!("Response: {:?}", response); - Ok(CallToolResult::success(vec![Content::text(format!( + Ok(CallToolResult::success(vec![ContentBlock::text(format!( "Question: {}\nAnswer: {}", question, response From 9a6f18259a26e27833150b8f8d1a5d7022068149 Mon Sep 17 00:00:00 2001 From: Dale Seo <5466341+DaleSeo@users.noreply.github.com> Date: Wed, 24 Jun 2026 17:39:13 -0400 Subject: [PATCH 2/2] fix: complete 2025-11-25 model spec conformance --- conformance/src/bin/client.rs | 4 +- crates/rmcp/src/handler/client.rs | 8 +- crates/rmcp/src/model.rs | 87 ++++++++++- crates/rmcp/src/model/content.rs | 1 - crates/rmcp/src/model/task.rs | 34 ++--- crates/rmcp/tests/test_completion.rs | 20 ++- .../client_json_rpc_message_schema.json | 140 +++++++++--------- ...lient_json_rpc_message_schema_current.json | 140 +++++++++--------- .../server_json_rpc_message_schema.json | 76 +++++++++- ...erver_json_rpc_message_schema_current.json | 76 +++++++++- crates/rmcp/tests/test_sampling.rs | 11 ++ crates/rmcp/tests/test_task.rs | 33 ++++- examples/clients/src/task_stdio.rs | 6 +- 13 files changed, 449 insertions(+), 187 deletions(-) diff --git a/conformance/src/bin/client.rs b/conformance/src/bin/client.rs index fc3d9f3bb..8dabff0ff 100644 --- a/conformance/src/bin/client.rs +++ b/conformance/src/bin/client.rs @@ -121,7 +121,9 @@ impl ClientHandler for ElicitationDefaultsClientHandler { _ => Some(json!({})), }; let mut result = ElicitResult::new(ElicitationAction::Accept); - result.content = content; + if let Some(c) = content { + result = result.with_content(c); + } Ok(result) } } diff --git a/crates/rmcp/src/handler/client.rs b/crates/rmcp/src/handler/client.rs index a7a29f04a..886268073 100644 --- a/crates/rmcp/src/handler/client.rs +++ b/crates/rmcp/src/handler/client.rs @@ -128,7 +128,7 @@ pub trait ClientHandler: Sized + Send + Sync + 'static { /// /// # Example /// ```rust,ignore - /// use rmcp::model::CreateElicitationRequestParam; + /// use rmcp::model::ElicitRequestParams; /// use rmcp::{ /// model::ErrorData as McpError, /// model::*, @@ -139,11 +139,11 @@ pub trait ClientHandler: Sized + Send + Sync + 'static { /// impl ClientHandler for MyClient { /// async fn create_elicitation( /// &self, - /// request: CreateElicitationRequestParam, + /// request: ElicitRequestParams, /// context: RequestContext, /// ) -> Result { /// match request { - /// CreateElicitationRequestParam::FormElicitationParam {meta, message, requested_schema,} => { + /// ElicitRequestParams::FormElicitationParam {meta, message, requested_schema,} => { /// // Display message to user and collect input according to requested_schema /// let user_input = get_user_input(message, requested_schema).await?; /// Ok(ElicitResult { @@ -152,7 +152,7 @@ pub trait ClientHandler: Sized + Send + Sync + 'static { /// meta: None, /// }) /// } - /// CreateElicitationRequestParam::UrlElicitationParam {meta, message, url, elicitation_id,} => { + /// ElicitRequestParams::UrlElicitationParam {meta, message, url, elicitation_id,} => { /// // Open URL in browser for user to complete elicitation /// open_url_in_browser(url).await?; /// Ok(ElicitResult { diff --git a/crates/rmcp/src/model.rs b/crates/rmcp/src/model.rs index 7f3ff38e2..cf2fe5900 100644 --- a/crates/rmcp/src/model.rs +++ b/crates/rmcp/src/model.rs @@ -1,4 +1,8 @@ -use std::{borrow::Cow, sync::Arc}; +use std::{ + borrow::Cow, + ops::{Deref, DerefMut}, + sync::Arc, +}; mod annotated; mod capabilities; mod content; @@ -702,6 +706,7 @@ impl CustomResult { pub struct CancelledNotificationParam { #[serde(skip_serializing_if = "Option::is_none")] pub request_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub reason: Option, #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] pub meta: Option, @@ -2417,7 +2422,6 @@ impl Reference { Self::Prompt(PromptReference { name: name.into(), title: None, - meta: None, }) } @@ -2474,8 +2478,6 @@ pub struct PromptReference { pub name: String, #[serde(skip_serializing_if = "Option::is_none")] pub title: Option, - #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, } impl PromptReference { @@ -2484,7 +2486,6 @@ impl PromptReference { Self { name: name.into(), title: None, - meta: None, } } @@ -2561,12 +2562,20 @@ pub type ListRootsRequest = RequestNoParam; #[non_exhaustive] pub struct ListRootsResult { pub roots: Vec, + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option, } impl ListRootsResult { /// Creates a new `ListRootsResult` with the given roots. pub fn new(roots: Vec) -> Self { - Self { roots } + Self { roots, meta: None } + } + + /// Sets the protocol-level metadata for this result. + pub fn with_meta(mut self, meta: Meta) -> Self { + self.meta = Some(meta); + self } } @@ -3370,7 +3379,47 @@ const_string!(TaskStatusNotificationMethod = "notifications/tasks/status"); /// Parameters for a task status notification (spec `TaskStatusNotificationParams`). /// /// The task fields are flattened at the top level: `NotificationParams & Task`. -pub type TaskStatusNotificationParam = crate::model::Task; +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[non_exhaustive] +pub struct TaskStatusNotificationParam { + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + pub meta: Option, + #[serde(flatten)] + pub task: crate::model::Task, +} + +impl TaskStatusNotificationParam { + pub fn new(task: crate::model::Task) -> Self { + Self { meta: None, task } + } + + pub fn with_meta(mut self, meta: Meta) -> Self { + self.meta = Some(meta); + self + } +} + +impl From for TaskStatusNotificationParam { + fn from(task: crate::model::Task) -> Self { + Self::new(task) + } +} + +impl Deref for TaskStatusNotificationParam { + type Target = crate::model::Task; + + fn deref(&self) -> &Self::Target { + &self.task + } +} + +impl DerefMut for TaskStatusNotificationParam { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.task + } +} pub type TaskStatusNotification = Notification; @@ -3613,6 +3662,30 @@ mod tests { use super::*; + #[test] + #[allow(deprecated)] + fn deprecated_aliases_still_resolve() { + // 하위호환: 구 이름이 새 타입으로 여전히 resolve되는지 확인. + let _: CreateElicitationResult = ElicitResult::new(ElicitationAction::Accept); + let _: GetTaskResultParams = GetTaskPayloadParams::new("task-1"); + let _: ResourceReference = ResourceTemplateReference::new("res://x"); + } + + #[test] + fn cancelled_notification_request_id_is_optional_on_wire() { + // None → requestId 생략 + let p = CancelledNotificationParam::new(None, Some("user cancelled".into())); + let v = serde_json::to_value(&p).unwrap(); + assert!(v.get("requestId").is_none()); + + // Some → requestId 방출 + 라운드트립 + let p = CancelledNotificationParam::new(Some(RequestId::Number(1)), None); + let v = serde_json::to_value(&p).unwrap(); + assert_eq!(v["requestId"], json!(1)); + let back: CancelledNotificationParam = serde_json::from_value(v).unwrap(); + assert_eq!(back.request_id, Some(RequestId::Number(1))); + } + #[test] fn test_notification_serde() { let raw = json!( { diff --git a/crates/rmcp/src/model/content.rs b/crates/rmcp/src/model/content.rs index 52887e250..c32f81b3d 100644 --- a/crates/rmcp/src/model/content.rs +++ b/crates/rmcp/src/model/content.rs @@ -195,7 +195,6 @@ pub struct ToolResultContent { #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] pub meta: Option, pub tool_use_id: String, - #[serde(default, skip_serializing_if = "Vec::is_empty")] pub content: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub structured_content: Option, diff --git a/crates/rmcp/src/model/task.rs b/crates/rmcp/src/model/task.rs index d81520c8c..8f4934258 100644 --- a/crates/rmcp/src/model/task.rs +++ b/crates/rmcp/src/model/task.rs @@ -138,12 +138,20 @@ impl Task { #[non_exhaustive] pub struct CreateTaskResult { pub task: Task, + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option, } impl CreateTaskResult { /// Create a new CreateTaskResult. pub fn new(task: Task) -> Self { - Self { task } + Self { task, meta: None } + } + + /// Sets the protocol-level metadata for this result. + pub fn with_meta(mut self, meta: Meta) -> Self { + self.meta = Some(meta); + self } } @@ -224,27 +232,3 @@ impl CancelTaskResult { Self { meta: None, task } } } - -/// Paginated list of tasks -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[non_exhaustive] -pub struct TaskList { - pub tasks: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub next_cursor: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub total: Option, -} - -impl TaskList { - /// Create a new TaskList. - pub fn new(tasks: Vec) -> Self { - Self { - tasks, - next_cursor: None, - total: None, - } - } -} diff --git a/crates/rmcp/tests/test_completion.rs b/crates/rmcp/tests/test_completion.rs index 9d64fae78..b155bd91b 100644 --- a/crates/rmcp/tests/test_completion.rs +++ b/crates/rmcp/tests/test_completion.rs @@ -156,14 +156,18 @@ fn test_completion_serialization_format() { #[test] fn test_resource_reference() { - // Test that ResourceTemplateReference works correctly - let resource_ref = ResourceTemplateReference::new("test://uri"); - - // Test that ResourceTemplateReference works correctly - let another_ref = ResourceTemplateReference::new("test://uri"); - - // They should be equivalent - assert_eq!(resource_ref.uri, another_ref.uri); + // ResourceTemplateReference가 `ref/resource` 와이어 태그로 직렬화/역직렬화되는지 확인 + let reference = Reference::for_resource("test://uri"); + + let json = serde_json::to_value(&reference).unwrap(); + assert_eq!(json["type"], "ref/resource"); + assert_eq!(json["uri"], "test://uri"); + + let back: Reference = serde_json::from_value(json).unwrap(); + match back { + Reference::Resource(r) => assert_eq!(r.uri, "test://uri"), + other => panic!("expected Reference::Resource, got {other:?}"), + } } #[test] 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 6a6da4cec..46378bc9c 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 @@ -1138,6 +1138,13 @@ "ListRootsResult": { "type": "object", "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "roots": { "type": "array", "items": { @@ -1210,7 +1217,7 @@ "$ref": "#/definitions/TaskStatusNotificationMethod" }, "params": { - "$ref": "#/definitions/Task" + "$ref": "#/definitions/TaskStatusNotificationParam" } }, "required": [ @@ -1329,13 +1336,6 @@ "PromptReference": { "type": "object", "properties": { - "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true - }, "name": { "type": "string" }, @@ -2112,63 +2112,6 @@ "uri" ] }, - "Task": { - "description": "Primary Task object that surfaces metadata during the task lifecycle.\n\nPer spec, `lastUpdatedAt` and `ttl` are required fields.\n`ttl` is nullable (`null` means unlimited retention).", - "type": "object", - "properties": { - "createdAt": { - "description": "ISO-8601 creation timestamp.", - "type": "string" - }, - "lastUpdatedAt": { - "description": "ISO-8601 timestamp for the most recent status change.", - "type": "string" - }, - "pollInterval": { - "description": "Suggested polling interval (milliseconds).", - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0 - }, - "status": { - "description": "Current lifecycle status (see [`TaskStatus`]).", - "allOf": [ - { - "$ref": "#/definitions/TaskStatus" - } - ] - }, - "statusMessage": { - "description": "Optional human-readable status message for UI surfaces.", - "type": [ - "string", - "null" - ] - }, - "taskId": { - "description": "Unique task identifier generated by the receiver.", - "type": "string" - }, - "ttl": { - "description": "Retention window in milliseconds that the receiver agreed to honor.\n`None` (serialized as `null`) means unlimited retention.", - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0 - } - }, - "required": [ - "taskId", - "status", - "createdAt", - "lastUpdatedAt" - ] - }, "TaskMetadata": { "description": "Metadata for augmenting a request with task execution (spec `TaskMetadata`).", "type": "object", @@ -2254,6 +2197,70 @@ "format": "const", "const": "notifications/tasks/status" }, + "TaskStatusNotificationParam": { + "description": "Parameters for a task status notification (spec `TaskStatusNotificationParams`).\n\nThe task fields are flattened at the top level: `NotificationParams & Task`.", + "type": "object", + "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "createdAt": { + "description": "ISO-8601 creation timestamp.", + "type": "string" + }, + "lastUpdatedAt": { + "description": "ISO-8601 timestamp for the most recent status change.", + "type": "string" + }, + "pollInterval": { + "description": "Suggested polling interval (milliseconds).", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "status": { + "description": "Current lifecycle status (see [`TaskStatus`]).", + "allOf": [ + { + "$ref": "#/definitions/TaskStatus" + } + ] + }, + "statusMessage": { + "description": "Optional human-readable status message for UI surfaces.", + "type": [ + "string", + "null" + ] + }, + "taskId": { + "description": "Unique task identifier generated by the receiver.", + "type": "string" + }, + "ttl": { + "description": "Retention window in milliseconds that the receiver agreed to honor.\n`None` (serialized as `null`) means unlimited retention.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "taskId", + "status", + "createdAt", + "lastUpdatedAt" + ] + }, "TasksCapability": { "description": "Task capabilities shared by client and server.", "type": "object", @@ -2351,7 +2358,8 @@ } }, "required": [ - "toolUseId" + "toolUseId", + "content" ] }, "ToolUseContent": { 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 6a6da4cec..46378bc9c 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 @@ -1138,6 +1138,13 @@ "ListRootsResult": { "type": "object", "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "roots": { "type": "array", "items": { @@ -1210,7 +1217,7 @@ "$ref": "#/definitions/TaskStatusNotificationMethod" }, "params": { - "$ref": "#/definitions/Task" + "$ref": "#/definitions/TaskStatusNotificationParam" } }, "required": [ @@ -1329,13 +1336,6 @@ "PromptReference": { "type": "object", "properties": { - "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true - }, "name": { "type": "string" }, @@ -2112,63 +2112,6 @@ "uri" ] }, - "Task": { - "description": "Primary Task object that surfaces metadata during the task lifecycle.\n\nPer spec, `lastUpdatedAt` and `ttl` are required fields.\n`ttl` is nullable (`null` means unlimited retention).", - "type": "object", - "properties": { - "createdAt": { - "description": "ISO-8601 creation timestamp.", - "type": "string" - }, - "lastUpdatedAt": { - "description": "ISO-8601 timestamp for the most recent status change.", - "type": "string" - }, - "pollInterval": { - "description": "Suggested polling interval (milliseconds).", - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0 - }, - "status": { - "description": "Current lifecycle status (see [`TaskStatus`]).", - "allOf": [ - { - "$ref": "#/definitions/TaskStatus" - } - ] - }, - "statusMessage": { - "description": "Optional human-readable status message for UI surfaces.", - "type": [ - "string", - "null" - ] - }, - "taskId": { - "description": "Unique task identifier generated by the receiver.", - "type": "string" - }, - "ttl": { - "description": "Retention window in milliseconds that the receiver agreed to honor.\n`None` (serialized as `null`) means unlimited retention.", - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0 - } - }, - "required": [ - "taskId", - "status", - "createdAt", - "lastUpdatedAt" - ] - }, "TaskMetadata": { "description": "Metadata for augmenting a request with task execution (spec `TaskMetadata`).", "type": "object", @@ -2254,6 +2197,70 @@ "format": "const", "const": "notifications/tasks/status" }, + "TaskStatusNotificationParam": { + "description": "Parameters for a task status notification (spec `TaskStatusNotificationParams`).\n\nThe task fields are flattened at the top level: `NotificationParams & Task`.", + "type": "object", + "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "createdAt": { + "description": "ISO-8601 creation timestamp.", + "type": "string" + }, + "lastUpdatedAt": { + "description": "ISO-8601 timestamp for the most recent status change.", + "type": "string" + }, + "pollInterval": { + "description": "Suggested polling interval (milliseconds).", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "status": { + "description": "Current lifecycle status (see [`TaskStatus`]).", + "allOf": [ + { + "$ref": "#/definitions/TaskStatus" + } + ] + }, + "statusMessage": { + "description": "Optional human-readable status message for UI surfaces.", + "type": [ + "string", + "null" + ] + }, + "taskId": { + "description": "Unique task identifier generated by the receiver.", + "type": "string" + }, + "ttl": { + "description": "Retention window in milliseconds that the receiver agreed to honor.\n`None` (serialized as `null`) means unlimited retention.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "taskId", + "status", + "createdAt", + "lastUpdatedAt" + ] + }, "TasksCapability": { "description": "Task capabilities shared by client and server.", "type": "object", @@ -2351,7 +2358,8 @@ } }, "required": [ - "toolUseId" + "toolUseId", + "content" ] }, "ToolUseContent": { 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 47d2ef835..2b3b41732 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 @@ -574,6 +574,13 @@ "description": "Wrapper returned by task-augmented requests (CreateTaskResult in SEP-1686).", "type": "object", "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "task": { "$ref": "#/definitions/Task" } @@ -1767,7 +1774,7 @@ "$ref": "#/definitions/TaskStatusNotificationMethod" }, "params": { - "$ref": "#/definitions/Task" + "$ref": "#/definitions/TaskStatusNotificationParam" } }, "required": [ @@ -2985,6 +2992,70 @@ "format": "const", "const": "notifications/tasks/status" }, + "TaskStatusNotificationParam": { + "description": "Parameters for a task status notification (spec `TaskStatusNotificationParams`).\n\nThe task fields are flattened at the top level: `NotificationParams & Task`.", + "type": "object", + "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "createdAt": { + "description": "ISO-8601 creation timestamp.", + "type": "string" + }, + "lastUpdatedAt": { + "description": "ISO-8601 timestamp for the most recent status change.", + "type": "string" + }, + "pollInterval": { + "description": "Suggested polling interval (milliseconds).", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "status": { + "description": "Current lifecycle status (see [`TaskStatus`]).", + "allOf": [ + { + "$ref": "#/definitions/TaskStatus" + } + ] + }, + "statusMessage": { + "description": "Optional human-readable status message for UI surfaces.", + "type": [ + "string", + "null" + ] + }, + "taskId": { + "description": "Unique task identifier generated by the receiver.", + "type": "string" + }, + "ttl": { + "description": "Retention window in milliseconds that the receiver agreed to honor.\n`None` (serialized as `null`) means unlimited retention.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "taskId", + "status", + "createdAt", + "lastUpdatedAt" + ] + }, "TaskSupport": { "description": "Per-tool task support mode as defined in the MCP specification.\n\nThis enum indicates whether a tool supports task-based invocation,\nallowing clients to know how to properly call the tool.\n\nSee [Tool-Level Negotiation](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#tool-level-negotiation).", "oneOf": [ @@ -3387,7 +3458,8 @@ } }, "required": [ - "toolUseId" + "toolUseId", + "content" ] }, "ToolUseContent": { 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 47d2ef835..2b3b41732 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 @@ -574,6 +574,13 @@ "description": "Wrapper returned by task-augmented requests (CreateTaskResult in SEP-1686).", "type": "object", "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "task": { "$ref": "#/definitions/Task" } @@ -1767,7 +1774,7 @@ "$ref": "#/definitions/TaskStatusNotificationMethod" }, "params": { - "$ref": "#/definitions/Task" + "$ref": "#/definitions/TaskStatusNotificationParam" } }, "required": [ @@ -2985,6 +2992,70 @@ "format": "const", "const": "notifications/tasks/status" }, + "TaskStatusNotificationParam": { + "description": "Parameters for a task status notification (spec `TaskStatusNotificationParams`).\n\nThe task fields are flattened at the top level: `NotificationParams & Task`.", + "type": "object", + "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "createdAt": { + "description": "ISO-8601 creation timestamp.", + "type": "string" + }, + "lastUpdatedAt": { + "description": "ISO-8601 timestamp for the most recent status change.", + "type": "string" + }, + "pollInterval": { + "description": "Suggested polling interval (milliseconds).", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "status": { + "description": "Current lifecycle status (see [`TaskStatus`]).", + "allOf": [ + { + "$ref": "#/definitions/TaskStatus" + } + ] + }, + "statusMessage": { + "description": "Optional human-readable status message for UI surfaces.", + "type": [ + "string", + "null" + ] + }, + "taskId": { + "description": "Unique task identifier generated by the receiver.", + "type": "string" + }, + "ttl": { + "description": "Retention window in milliseconds that the receiver agreed to honor.\n`None` (serialized as `null`) means unlimited retention.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "taskId", + "status", + "createdAt", + "lastUpdatedAt" + ] + }, "TaskSupport": { "description": "Per-tool task support mode as defined in the MCP specification.\n\nThis enum indicates whether a tool supports task-based invocation,\nallowing clients to know how to properly call the tool.\n\nSee [Tool-Level Negotiation](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#tool-level-negotiation).", "oneOf": [ @@ -3387,7 +3458,8 @@ } }, "required": [ - "toolUseId" + "toolUseId", + "content" ] }, "ToolUseContent": { diff --git a/crates/rmcp/tests/test_sampling.rs b/crates/rmcp/tests/test_sampling.rs index 283826897..83d3f6fb1 100644 --- a/crates/rmcp/tests/test_sampling.rs +++ b/crates/rmcp/tests/test_sampling.rs @@ -359,6 +359,17 @@ async fn test_tool_result_content_serialization() -> Result<()> { Ok(()) } +#[test] +fn test_tool_result_content_requires_content() { + let raw = serde_json::json!({ + "toolUseId": "call_123" + }); + + let err = serde_json::from_value::(raw).unwrap_err(); + + assert!(err.to_string().contains("missing field `content`")); +} + #[tokio::test] async fn test_sampling_message_with_tool_use() -> Result<()> { let message = SamplingMessage::assistant_tool_use( diff --git a/crates/rmcp/tests/test_task.rs b/crates/rmcp/tests/test_task.rs index 9ad0b2006..ca0f4af50 100644 --- a/crates/rmcp/tests/test_task.rs +++ b/crates/rmcp/tests/test_task.rs @@ -1,8 +1,12 @@ use std::{any::Any, time::Duration}; -use rmcp::task_manager::{ - OperationDescriptor, OperationMessage, OperationProcessor, OperationResultTransport, +use rmcp::{ + model::TaskStatusNotificationParam, + task_manager::{ + OperationDescriptor, OperationMessage, OperationProcessor, OperationResultTransport, + }, }; +use serde_json::json; struct DummyTransport { id: String, @@ -75,3 +79,28 @@ async fn rejects_duplicate_operation_ids() { .expect_err("duplicate should fail"); assert!(format!("{err}").contains("already running")); } + +#[test] +fn task_status_notification_param_preserves_meta() { + let raw = json!({ + "_meta": { + "traceId": "trace-1" + }, + "taskId": "task-1", + "status": "working", + "createdAt": "2026-06-24T00:00:00Z", + "lastUpdatedAt": "2026-06-24T00:00:01Z", + "ttl": null + }); + + let params: TaskStatusNotificationParam = serde_json::from_value(raw).unwrap(); + + assert_eq!(params.task.task_id, "task-1"); + assert_eq!(params.task_id, "task-1"); + assert_eq!(params.meta.as_ref().unwrap().0["traceId"], json!("trace-1")); + + let serialized = serde_json::to_value(¶ms).unwrap(); + + assert_eq!(serialized["_meta"]["traceId"], json!("trace-1")); + assert_eq!(serialized["taskId"], json!("task-1")); +} diff --git a/examples/clients/src/task_stdio.rs b/examples/clients/src/task_stdio.rs index b384ff29a..ffcc0dc22 100644 --- a/examples/clients/src/task_stdio.rs +++ b/examples/clients/src/task_stdio.rs @@ -53,9 +53,9 @@ async fn main() -> Result<()> { .await?; tracing::info!("quick_echo -> {echo:#?}"); - // 2) Task call. `slow_sum` is task_support = required, so we MUST attach a - // `task` object. An empty object is fine — clients can stash arbitrary - // metadata here that the server-side `OperationDescriptor` will keep. + // 2) Task call. `slow_sum` is task_support = required, so we MUST attach + // `task` metadata. An empty `TaskMetadata` is fine; use `.with_ttl(...)` + // to set a retention window. let create = client .send_request(ClientRequest::CallToolRequest(Request::new( CallToolRequestParams::new("slow_sum")