diff --git a/conformance/src/bin/client.rs b/conformance/src/bin/client.rs index 25345172..49afb75f 100644 --- a/conformance/src/bin/client.rs +++ b/conformance/src/bin/client.rs @@ -122,6 +122,7 @@ impl ClientHandler for ElicitationDefaultsClientHandler { Ok(CreateElicitationResult { action: ElicitationAction::Accept, content, + meta: None, }) } } @@ -174,6 +175,7 @@ impl ClientHandler for FullClientHandler { Ok(CreateElicitationResult { action: ElicitationAction::Accept, content: Some(json!({"username": "testuser", "email": "test@example.com"})), + meta: None, }) } } diff --git a/crates/rmcp/src/handler/client.rs b/crates/rmcp/src/handler/client.rs index 1b9c1e38..926aafcb 100644 --- a/crates/rmcp/src/handler/client.rs +++ b/crates/rmcp/src/handler/client.rs @@ -146,6 +146,7 @@ pub trait ClientHandler: Sized + Send + Sync + 'static { /// Ok(CreateElicitationResult { /// action: ElicitationAction::Accept, /// content: Some(user_input), + /// meta: None, /// }) /// } /// CreateElicitationRequestParam::UrlElicitationParam {meta, message, url, elicitation_id,} => { @@ -154,6 +155,7 @@ pub trait ClientHandler: Sized + Send + Sync + 'static { /// Ok(CreateElicitationResult { /// action: ElicitationAction::Accept, /// content: None, + /// meta: None, /// }) /// } /// } @@ -171,6 +173,7 @@ pub trait ClientHandler: Sized + Send + Sync + 'static { std::future::ready(Ok(CreateElicitationResult { action: ElicitationAction::Decline, content: None, + meta: None, })) } diff --git a/crates/rmcp/src/model.rs b/crates/rmcp/src/model.rs index 8859bbbc..001f4b60 100644 --- a/crates/rmcp/src/model.rs +++ b/crates/rmcp/src/model.rs @@ -2699,6 +2699,10 @@ pub struct CreateElicitationResult { /// Only present when action is Accept. #[serde(skip_serializing_if = "Option::is_none")] pub content: Option, + + /// Optional protocol-level metadata for this result. + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option, } impl CreateElicitationResult { @@ -2707,6 +2711,7 @@ impl CreateElicitationResult { Self { action, content: None, + meta: None, } } @@ -2715,6 +2720,12 @@ impl CreateElicitationResult { self.content = Some(content); self } + + /// Set the metadata on this result. + pub fn with_meta(mut self, meta: Meta) -> Self { + self.meta = Some(meta); + self + } } /// Request type for creating an elicitation to gather user input diff --git a/crates/rmcp/tests/test_elicitation.rs b/crates/rmcp/tests/test_elicitation.rs index bfa8cc49..04a112f5 100644 --- a/crates/rmcp/tests/test_elicitation.rs +++ b/crates/rmcp/tests/test_elicitation.rs @@ -1,6 +1,6 @@ //cargo test --test test_elicitation --features "client server" -use rmcp::{model::*, service::*}; +use rmcp::{model::*, object, service::*}; // For typed elicitation tests #[cfg(feature = "schemars")] use schemars::JsonSchema; @@ -98,6 +98,7 @@ async fn test_elicitation_result_serialization() { let accept_result = CreateElicitationResult { action: ElicitationAction::Accept, content: Some(json!({"email": "user@example.com"})), + meta: None, }; let json = serde_json::to_value(&accept_result).unwrap(); @@ -111,6 +112,7 @@ async fn test_elicitation_result_serialization() { let decline_result = CreateElicitationResult { action: ElicitationAction::Decline, content: None, + meta: None, }; let json = serde_json::to_value(&decline_result).unwrap(); @@ -124,6 +126,26 @@ async fn test_elicitation_result_serialization() { let deserialized: CreateElicitationResult = 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 json = serde_json::to_value(&meta_result).unwrap(); + let expected = json!({ + "action": "accept", + "_meta": {"traceId": "elicitation-123"} + }); + assert_eq!(json, expected); + + let deserialized: CreateElicitationResult = serde_json::from_value(expected).unwrap(); + assert_eq!( + deserialized.meta, + Some(Meta(object!({ "traceId": "elicitation-123" }))) + ); } /// Test that elicitation requests can be created and handled through the JSON-RPC protocol @@ -843,6 +865,7 @@ async fn test_elicitation_direction_server_to_client() { let client_result = ClientResult::CreateElicitationResult(CreateElicitationResult { action: ElicitationAction::Accept, content: Some(json!("John Doe")), + meta: None, }); // Verify client result can be serialized @@ -893,6 +916,7 @@ async fn test_elicitation_json_rpc_direction() { ClientResult::CreateElicitationResult(CreateElicitationResult { action: ElicitationAction::Accept, content: Some(json!(true)), + meta: None, }), RequestId::Number(1), ); @@ -928,6 +952,7 @@ async fn test_elicitation_actions_compliance() { ElicitationAction::Accept => Some(serde_json::json!("some data")), _ => None, }, + meta: None, }; let json = serde_json::to_value(&result).unwrap(); @@ -958,6 +983,7 @@ async fn test_elicitation_result_in_client_result() { let result = ClientResult::CreateElicitationResult(CreateElicitationResult { action: ElicitationAction::Decline, content: None, + meta: None, }); match result { @@ -2161,6 +2187,7 @@ async fn test_url_elicitation_action_workflow() { let accept_result = CreateElicitationResult { action: ElicitationAction::Accept, content: None, // URL elicitation doesn't return content, just confirmation + meta: None, }; let json = serde_json::to_value(&accept_result).unwrap(); @@ -2172,6 +2199,7 @@ async fn test_url_elicitation_action_workflow() { let decline_result = CreateElicitationResult { action: ElicitationAction::Decline, content: None, + meta: None, }; let json = serde_json::to_value(&decline_result).unwrap(); @@ -2181,6 +2209,7 @@ async fn test_url_elicitation_action_workflow() { let cancel_result = CreateElicitationResult { action: ElicitationAction::Cancel, content: None, + meta: None, }; let json = serde_json::to_value(&cancel_result).unwrap(); diff --git a/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json b/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json index 397fbe8b..8e082db9 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 @@ -418,6 +418,14 @@ "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": [ 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 397fbe8b..8e082db9 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 @@ -418,6 +418,14 @@ "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": [ 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 db21c2ba..405b3e02 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 @@ -655,6 +655,14 @@ "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": [ 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 10f45c0a..405b3e02 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 @@ -388,6 +388,7 @@ "content": { "description": "The content returned by the tool (text, images, etc.)", "type": "array", + "default": [], "items": { "$ref": "#/definitions/Annotated" } @@ -402,10 +403,7 @@ "structuredContent": { "description": "An optional JSON object that represents the structured result of the tool call" } - }, - "required": [ - "content" - ] + } }, "CancelTaskResult": { "description": "Response to a `tasks/cancel` request.\n\nPer spec, `CancelTaskResult = allOf[Result, Task]` — same shape as `GetTaskResult`.", @@ -657,6 +655,14 @@ "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": [