diff --git a/conformance/src/bin/server.rs b/conformance/src/bin/server.rs index c3424f612..0fc8abfdf 100644 --- a/conformance/src/bin/server.rs +++ b/conformance/src/bin/server.rs @@ -204,9 +204,8 @@ impl ServerHandler for ConformanceServer { ), ]; Ok(ListToolsResult { - meta: None, tools, - next_cursor: None, + ..Default::default() }) } @@ -543,7 +542,6 @@ impl ServerHandler for ConformanceServer { _cx: RequestContext, ) -> Result { Ok(ListResourcesResult { - meta: None, resources: vec![ RawResource { uri: "test://static-text".into(), @@ -568,7 +566,7 @@ impl ServerHandler for ConformanceServer { } .no_annotation(), ], - next_cursor: None, + ..Default::default() }) } @@ -628,7 +626,6 @@ impl ServerHandler for ConformanceServer { _cx: RequestContext, ) -> Result { Ok(ListResourceTemplatesResult { - meta: None, resource_templates: vec![ RawResourceTemplate { uri_template: "test://template/{id}/data".into(), @@ -640,7 +637,7 @@ impl ServerHandler for ConformanceServer { } .no_annotation(), ], - next_cursor: None, + ..Default::default() }) } @@ -670,7 +667,6 @@ impl ServerHandler for ConformanceServer { _cx: RequestContext, ) -> Result { Ok(ListPromptsResult { - meta: None, prompts: vec![ Prompt::new( "test_simple_prompt", @@ -700,7 +696,7 @@ impl ServerHandler for ConformanceServer { None, ), ], - next_cursor: None, + ..Default::default() }) } diff --git a/crates/rmcp-macros/src/prompt_handler.rs b/crates/rmcp-macros/src/prompt_handler.rs index 4f2541ac6..8a64079a4 100644 --- a/crates/rmcp-macros/src/prompt_handler.rs +++ b/crates/rmcp-macros/src/prompt_handler.rs @@ -61,6 +61,7 @@ pub fn prompt_handler(attr: TokenStream, input: TokenStream) -> syn::Result Result { let prompts = #router_expr.list_all(); Ok(ListPromptsResult { + result_type: Default::default(), prompts, meta: #meta, next_cursor: None, diff --git a/crates/rmcp-macros/src/tool_handler.rs b/crates/rmcp-macros/src/tool_handler.rs index 0cb323b5a..dc935828d 100644 --- a/crates/rmcp-macros/src/tool_handler.rs +++ b/crates/rmcp-macros/src/tool_handler.rs @@ -69,6 +69,7 @@ pub fn tool_handler(attr: TokenStream, input: TokenStream) -> syn::Result, ) -> Result { Ok(rmcp::model::ListToolsResult{ + result_type: Default::default(), tools: #router.list_all(), meta: #result_meta, next_cursor: None, diff --git a/crates/rmcp/src/handler/client.rs b/crates/rmcp/src/handler/client.rs index 926aafcb5..c2e919cf1 100644 --- a/crates/rmcp/src/handler/client.rs +++ b/crates/rmcp/src/handler/client.rs @@ -64,7 +64,9 @@ impl Service for H { ServerNotification::PromptListChangedNotification(_notification_no_param) => { self.on_prompt_list_changed(context).await } - ServerNotification::ElicitationCompletionNotification(notification) => { + ServerNotification::ElicitationCompletionNotification(notification) => + { + #[allow(deprecated)] self.on_url_elicitation_notification_complete(notification.params, context) .await } @@ -238,6 +240,10 @@ pub trait ClientHandler: Sized + Send + Sync + 'static { std::future::ready(()) } + #[deprecated( + since = "2.0.0", + note = "URL elicitation is removed by SEP-2322 (Multi Round-Trip Requests). Use InputRequiredResult-based MRTR flow instead." + )] fn on_url_elicitation_notification_complete( &self, params: ElicitationResponseNotificationParam, diff --git a/crates/rmcp/src/handler/server/prompt.rs b/crates/rmcp/src/handler/server/prompt.rs index 11ca4bf83..908a47d62 100644 --- a/crates/rmcp/src/handler/server/prompt.rs +++ b/crates/rmcp/src/handler/server/prompt.rs @@ -103,6 +103,7 @@ impl IntoGetPromptResult for GetPromptResult { impl IntoGetPromptResult for Vec { fn into_get_prompt_result(self) -> Result { Ok(GetPromptResult { + result_type: Default::default(), description: None, messages: self, }) diff --git a/crates/rmcp/src/handler/server/router/tool.rs b/crates/rmcp/src/handler/server/router/tool.rs index 35bd25a97..eeb3f5b78 100644 --- a/crates/rmcp/src/handler/server/router/tool.rs +++ b/crates/rmcp/src/handler/server/router/tool.rs @@ -667,6 +667,8 @@ mod tests { name: Cow::Borrowed("requires_params"), arguments: Some(Default::default()), task: None, + input_responses: None, + request_state: None, }, RequestContext::new(NumberOrString::Number(1), peer), ); @@ -706,6 +708,8 @@ mod tests { name: Cow::Borrowed("test_tool"), arguments: None, task: None, + input_responses: None, + request_state: None, }, RequestContext::new(NumberOrString::Number(1), peer), ); diff --git a/crates/rmcp/src/handler/server/tool.rs b/crates/rmcp/src/handler/server/tool.rs index d5b75a8f2..6d15f43f2 100644 --- a/crates/rmcp/src/handler/server/tool.rs +++ b/crates/rmcp/src/handler/server/tool.rs @@ -46,6 +46,7 @@ impl<'s, S> ToolCallContext<'s, S> { name, arguments, task, + .. }: CallToolRequestParams, request_context: RequestContext, ) -> Self { diff --git a/crates/rmcp/src/model.rs b/crates/rmcp/src/model.rs index 5fb7eb9ed..e095023da 100644 --- a/crates/rmcp/src/model.rs +++ b/crates/rmcp/src/model.rs @@ -5,6 +5,7 @@ mod content; mod elicitation_schema; mod extension; mod meta; +mod mrtr; mod prompt; mod resource; mod serde_impl; @@ -16,6 +17,7 @@ pub use content::*; pub use elicitation_schema::*; pub use extension::*; pub use meta::*; +pub use mrtr::*; pub use prompt::*; pub use resource::*; use serde::{Deserialize, Serialize, de::DeserializeOwned}; @@ -509,6 +511,10 @@ impl ErrorCode { pub const INVALID_PARAMS: Self = Self(-32602); pub const INTERNAL_ERROR: Self = Self(-32603); pub const PARSE_ERROR: Self = Self(-32700); + #[deprecated( + since = "2.0.0", + note = "URLElicitationRequiredError is removed by SEP-2322 (Multi Round-Trip Requests). Use InputRequiredResult instead." + )] pub const URL_ELICITATION_REQUIRED: Self = Self(-32042); } @@ -565,6 +571,11 @@ impl ErrorData { pub fn internal_error(message: impl Into>, data: Option) -> Self { Self::new(ErrorCode::INTERNAL_ERROR, message, data) } + #[deprecated( + since = "2.0.0", + note = "URLElicitationRequiredError is removed by SEP-2322 (Multi Round-Trip Requests). Use InputRequiredResult instead." + )] + #[allow(deprecated)] pub fn url_elicitation_required( message: impl Into>, data: Option, @@ -677,6 +688,71 @@ impl From for () { fn from(_value: EmptyResult) {} } +/// Indicates the type of a result object, allowing the client to +/// determine how to parse the response. +/// +/// The spec defines this as an open string (`"complete" | "input_required" | string`), +/// so unknown values are preserved rather than rejected. Servers implementing this +/// protocol version MUST include `resultType` in every result. For backward +/// compatibility, clients MUST treat an absent field as `"complete"`. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct ResultType(Cow<'static, str>); + +impl ResultType { + pub const COMPLETE: Self = Self(Cow::Borrowed("complete")); + pub const INPUT_REQUIRED: Self = Self(Cow::Borrowed("input_required")); + + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Returns `true` if this is `"input_required"`. + pub fn is_input_required(&self) -> bool { + self.0 == "input_required" + } + + /// Returns `true` if this is `"complete"`. + pub fn is_complete(&self) -> bool { + self.0 == "complete" + } +} + +impl Default for ResultType { + fn default() -> Self { + Self::COMPLETE + } +} + +impl Serialize for ResultType { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.0.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for ResultType { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s: String = Deserialize::deserialize(deserializer)?; + match s.as_str() { + "complete" => Ok(Self::COMPLETE), + "input_required" => Ok(Self::INPUT_REQUIRED), + _ => Ok(Self(Cow::Owned(s))), + } + } +} + +impl std::fmt::Display for ResultType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + /// A catch-all response either side can use for custom requests. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(transparent)] @@ -1157,6 +1233,9 @@ macro_rules! paginated_result { #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct $t { + /// Result type discriminator. Always serialized; older peers ignore it. + #[serde(default)] + pub result_type: ResultType, #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] pub meta: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -1169,6 +1248,7 @@ macro_rules! paginated_result { items: $t_item, ) -> Self { Self { + result_type: ResultType::default(), meta: None, next_cursor: None, $i_item: items, @@ -1212,6 +1292,13 @@ pub struct ReadResourceRequestParams { pub meta: Option, /// The URI of the resource to read pub uri: String, + /// Client responses to server-initiated input requests from a previous + /// [`InputRequiredResult`]. + #[serde(skip_serializing_if = "Option::is_none")] + pub input_responses: Option, + /// Opaque request state echoed back from a previous [`InputRequiredResult`]. + #[serde(skip_serializing_if = "Option::is_none")] + pub request_state: Option, } impl ReadResourceRequestParams { @@ -1220,6 +1307,8 @@ impl ReadResourceRequestParams { Self { meta: None, uri: uri.into(), + input_responses: None, + request_state: None, } } @@ -1228,6 +1317,18 @@ impl ReadResourceRequestParams { self.meta = Some(meta); self } + + /// Sets the input responses for an MRTR retry. + pub fn with_input_responses(mut self, input_responses: InputResponses) -> Self { + self.input_responses = Some(input_responses); + self + } + + /// Sets the request state for an MRTR retry. + pub fn with_request_state(mut self, request_state: impl Into) -> Self { + self.request_state = Some(request_state.into()); + self + } } impl RequestParamsMeta for ReadResourceRequestParams { @@ -1248,6 +1349,9 @@ pub type ReadResourceRequestParam = ReadResourceRequestParams; #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[non_exhaustive] pub struct ReadResourceResult { + /// Result type discriminator. Always serialized; older peers ignore it. + #[serde(default)] + pub result_type: ResultType, /// The actual content of the resource pub contents: Vec, } @@ -1255,7 +1359,10 @@ pub struct ReadResourceResult { impl ReadResourceResult { /// Create a new ReadResourceResult with the given contents. pub fn new(contents: Vec) -> Self { - Self { contents } + Self { + result_type: ResultType::default(), + contents, + } } } @@ -1395,6 +1502,13 @@ pub struct GetPromptRequestParams { pub name: String, #[serde(skip_serializing_if = "Option::is_none")] pub arguments: Option, + /// Client responses to server-initiated input requests from a previous + /// [`InputRequiredResult`]. + #[serde(skip_serializing_if = "Option::is_none")] + pub input_responses: Option, + /// Opaque request state echoed back from a previous [`InputRequiredResult`]. + #[serde(skip_serializing_if = "Option::is_none")] + pub request_state: Option, } impl GetPromptRequestParams { @@ -1404,6 +1518,8 @@ impl GetPromptRequestParams { meta: None, name: name.into(), arguments: None, + input_responses: None, + request_state: None, } } @@ -1418,6 +1534,18 @@ impl GetPromptRequestParams { self.meta = Some(meta); self } + + /// Sets the input responses for an MRTR retry. + pub fn with_input_responses(mut self, input_responses: InputResponses) -> Self { + self.input_responses = Some(input_responses); + self + } + + /// Sets the request state for an MRTR retry. + pub fn with_request_state(mut self, request_state: impl Into) -> Self { + self.request_state = Some(request_state.into()); + self + } } impl RequestParamsMeta for GetPromptRequestParams { @@ -2353,13 +2481,19 @@ impl CompletionInfo { #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[non_exhaustive] pub struct CompleteResult { + /// Result type discriminator. Always serialized; older peers ignore it. + #[serde(default)] + pub result_type: ResultType, pub completion: CompletionInfo, } impl CompleteResult { /// Create a new CompleteResult with the given completion info. pub fn new(completion: CompletionInfo) -> Self { - Self { completion } + Self { + result_type: ResultType::default(), + completion, + } } } @@ -2778,6 +2912,9 @@ pub type ElicitationCompletionNotification = #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[non_exhaustive] pub struct CallToolResult { + /// Result type discriminator. Always serialized; older peers ignore it. + #[serde(default)] + pub result_type: ResultType, /// The content returned by the tool (text, images, etc.) #[serde(default)] pub content: Vec, @@ -2805,6 +2942,8 @@ impl<'de> Deserialize<'de> for CallToolResult { #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct Helper { + #[serde(default)] + result_type: ResultType, content: Option>, structured_content: Option, is_error: Option, @@ -2826,6 +2965,7 @@ impl<'de> Deserialize<'de> for CallToolResult { } Ok(CallToolResult { + result_type: helper.result_type, content: helper.content.unwrap_or_default(), structured_content: helper.structured_content, is_error: helper.is_error, @@ -2838,6 +2978,7 @@ impl CallToolResult { /// Create a successful tool result with unstructured content pub fn success(content: Vec) -> Self { CallToolResult { + result_type: ResultType::default(), content, structured_content: None, is_error: Some(false), @@ -2895,6 +3036,7 @@ impl CallToolResult { /// ``` pub fn error(content: Vec) -> Self { CallToolResult { + result_type: ResultType::default(), content, structured_content: None, is_error: Some(true), @@ -2917,6 +3059,7 @@ impl CallToolResult { /// ``` pub fn structured(value: Value) -> Self { CallToolResult { + result_type: ResultType::default(), content: vec![Content::text(value.to_string())], structured_content: Some(value), is_error: Some(false), @@ -2943,6 +3086,7 @@ impl CallToolResult { /// ``` pub fn structured_error(value: Value) -> Self { CallToolResult { + result_type: ResultType::default(), content: vec![Content::text(value.to_string())], structured_content: Some(value), is_error: Some(true), @@ -3019,6 +3163,14 @@ pub struct CallToolRequestParams { /// Task metadata for async task management (SEP-1319) #[serde(skip_serializing_if = "Option::is_none")] pub task: Option, + /// Client responses to server-initiated input requests from a previous + /// [`InputRequiredResult`]. Present only when retrying after an incomplete result. + #[serde(skip_serializing_if = "Option::is_none")] + pub input_responses: Option, + /// Opaque request state echoed back from a previous [`InputRequiredResult`]. + /// Clients MUST return this value exactly as received. + #[serde(skip_serializing_if = "Option::is_none")] + pub request_state: Option, } impl CallToolRequestParams { @@ -3029,6 +3181,8 @@ impl CallToolRequestParams { name: name.into(), arguments: None, task: None, + input_responses: None, + request_state: None, } } @@ -3043,6 +3197,18 @@ impl CallToolRequestParams { self.task = Some(task); self } + + /// Sets the input responses for an MRTR retry. + pub fn with_input_responses(mut self, input_responses: InputResponses) -> Self { + self.input_responses = Some(input_responses); + self + } + + /// Sets the request state for an MRTR retry. + pub fn with_request_state(mut self, request_state: impl Into) -> Self { + self.request_state = Some(request_state.into()); + self + } } impl RequestParamsMeta for CallToolRequestParams { @@ -3131,6 +3297,9 @@ impl CreateMessageResult { #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[non_exhaustive] pub struct GetPromptResult { + /// Result type discriminator. Always serialized; older peers ignore it. + #[serde(default)] + pub result_type: ResultType, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, pub messages: Vec, @@ -3140,6 +3309,7 @@ impl GetPromptResult { /// Create a new GetPromptResult with required fields. pub fn new(messages: Vec) -> Self { Self { + result_type: ResultType::default(), description: None, messages, } @@ -3431,6 +3601,7 @@ ts_union!( | GetTaskResult | CancelTaskResult | CallToolResult + | InputRequiredResult | GetTaskPayloadResult | EmptyResult | CustomResult diff --git a/crates/rmcp/src/model/mrtr.rs b/crates/rmcp/src/model/mrtr.rs new file mode 100644 index 000000000..4a50457a4 --- /dev/null +++ b/crates/rmcp/src/model/mrtr.rs @@ -0,0 +1,388 @@ +//! Multi Round-Trip Request (MRTR) types for SEP-2322. +//! +//! Provides [`InputRequiredResult`], [`InputRequests`], and [`InputResponses`] +//! for the stateless multi round-trip request pattern defined in the MCP spec. +//! [`ResultType`] lives in the parent [`super`] module alongside other base result types. +//! +//! # Overview +//! +//! A server may respond to `tools/call`, `prompts/get`, or `resources/read` with an +//! [`InputRequiredResult`] instead of the normal result. The client fulfills the +//! [`InputRequests`], then retries the original request with [`InputResponses`] and +//! the echoed `requestState`. + +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use super::{CreateElicitationRequest, CreateMessageRequest, ListRootsRequest, Meta, ResultType}; + +/// A server-initiated request that can appear inside [`InputRequests`]. +/// +/// Per the MCP spec, only `CreateMessageRequest` (sampling), +/// `CreateElicitationRequest` (elicitation), and `ListRootsRequest` (roots) +/// are allowed. This is modeled as an untagged enum rather than a +/// `ServerRequest` alias to prevent `PingRequest` or `CustomRequest` from +/// being included. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[non_exhaustive] +pub enum InputRequest { + /// A `sampling/createMessage` request. + CreateMessage(CreateMessageRequest), + /// An `elicitation/create` request. + Elicitation(CreateElicitationRequest), + /// A `roots/list` request. + ListRoots(ListRootsRequest), +} + +/// A map of server-initiated requests that the client must fulfill. +/// +/// Keys are server-assigned string identifiers; values are request objects +/// (`ElicitRequest`, `CreateMessageRequest`, or `ListRootsRequest`). +pub type InputRequests = BTreeMap; + +/// A map of client responses to server-initiated requests. +/// +/// Keys correspond to the keys in the [`InputRequests`] map; values are the +/// client's result for each request (`ElicitResult`, `CreateMessageResult`, +/// or `ListRootsResult`), represented as opaque JSON because the +/// heterogeneous `ClientResult` union does not derive the traits required +/// for use as a `BTreeMap` value. +pub type InputResponses = BTreeMap; + +/// A result indicating that additional input is needed before the request +/// can be completed. +/// +/// At least one of [`input_requests`](Self::input_requests) or +/// [`request_state`](Self::request_state) MUST be present. +/// +/// Servers MAY send this in response to `tools/call`, `prompts/get`, or +/// `resources/read`. Servers MUST NOT send this for any other request. +/// +/// # Examples +/// +/// ``` +/// use rmcp::model::InputRequiredResult; +/// +/// let result = InputRequiredResult::from_request_state("opaque-server-state"); +/// assert!(result.input_requests.is_none()); +/// assert_eq!(result.request_state.as_deref(), Some("opaque-server-state")); +/// ``` +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[non_exhaustive] +pub struct InputRequiredResult { + /// Always `"input_required"` for this result type. + pub result_type: ResultType, + + /// Server-initiated requests that the client must fulfill before retrying. + #[serde(skip_serializing_if = "Option::is_none")] + pub input_requests: Option, + + /// Opaque request state to be echoed back by the client on retry. + /// Clients MUST NOT inspect, parse, modify, or make any assumptions + /// about the contents. + #[serde(skip_serializing_if = "Option::is_none")] + pub request_state: Option, + + /// Optional protocol-level metadata. + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option, +} + +/// Custom deserializer that requires `resultType: "input_required"` to prevent +/// greedy matching in the untagged `ServerResult` enum (which would otherwise +/// swallow empty objects or unknown shapes). +impl<'de> Deserialize<'de> for InputRequiredResult { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct Helper { + result_type: Option, + input_requests: Option, + request_state: Option, + #[serde(rename = "_meta")] + meta: Option, + } + + let helper = Helper::deserialize(deserializer)?; + + match &helper.result_type { + Some(rt) if rt.is_input_required() => {} + _ => { + return Err(serde::de::Error::custom( + "InputRequiredResult requires resultType to be \"input_required\"", + )); + } + } + + Ok(InputRequiredResult { + result_type: ResultType::INPUT_REQUIRED, + input_requests: helper.input_requests, + request_state: helper.request_state, + meta: helper.meta, + }) + } +} + +impl InputRequiredResult { + /// Creates a new `InputRequiredResult` with both input requests and request state. + pub fn new(input_requests: Option, request_state: Option) -> Self { + Self { + result_type: ResultType::INPUT_REQUIRED, + input_requests, + request_state, + meta: None, + } + } + + /// Creates from input requests only. + pub fn from_input_requests(input_requests: InputRequests) -> Self { + Self::new(Some(input_requests), None) + } + + /// Creates from request state only (e.g. for load shedding). + pub fn from_request_state(request_state: impl Into) -> Self { + Self::new(None, Some(request_state.into())) + } + + /// Sets optional metadata. + pub fn with_meta(mut self, meta: Meta) -> Self { + self.meta = Some(meta); + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + mod result_type { + use super::*; + + #[test] + fn default_is_complete() { + assert_eq!(ResultType::default(), ResultType::COMPLETE); + } + + #[test] + fn serializes_complete() { + assert_eq!( + serde_json::to_value(&ResultType::COMPLETE).unwrap(), + serde_json::json!("complete") + ); + } + + #[test] + fn serializes_input_required() { + assert_eq!( + serde_json::to_value(&ResultType::INPUT_REQUIRED).unwrap(), + serde_json::json!("input_required") + ); + } + + #[test] + fn deserializes_known_values() { + let complete: ResultType = + serde_json::from_value(serde_json::json!("complete")).unwrap(); + assert_eq!(complete, ResultType::COMPLETE); + + let input_required: ResultType = + serde_json::from_value(serde_json::json!("input_required")).unwrap(); + assert_eq!(input_required, ResultType::INPUT_REQUIRED); + } + + #[test] + fn preserves_unknown_extension_values() { + let custom: ResultType = + serde_json::from_value(serde_json::json!("streaming")).unwrap(); + assert_eq!(custom.as_str(), "streaming"); + assert!(!custom.is_complete()); + assert!(!custom.is_input_required()); + + let reserialized = serde_json::to_value(&custom).unwrap(); + assert_eq!(reserialized, serde_json::json!("streaming")); + } + } + + mod input_required_result { + use super::*; + + #[test] + fn deserializes_with_requests_and_state() { + let json = serde_json::json!({ + "resultType": "input_required", + "inputRequests": { + "github_login": { + "method": "elicitation/create", + "params": { + "message": "Please provide your GitHub username", + "requestedSchema": { + "type": "object", + "properties": { "name": { "type": "string" } }, + "required": ["name"] + } + } + }, + "capital_of_france": { + "method": "sampling/createMessage", + "params": { + "messages": [{ + "role": "user", + "content": { "type": "text", "text": "What is the capital of France?" } + }], + "maxTokens": 100 + } + } + }, + "requestState": "eyJsb2NhdGlvbiI6Ik5ldyBZb3JrIn0" + }); + + let result: InputRequiredResult = serde_json::from_value(json).unwrap(); + + let requests = result + .input_requests + .as_ref() + .expect("should have input_requests"); + assert_eq!(requests.len(), 2); + assert!(requests.contains_key("github_login")); + assert!(requests.contains_key("capital_of_france")); + assert_eq!( + result.request_state.as_deref(), + Some("eyJsb2NhdGlvbiI6Ik5ldyBZb3JrIn0") + ); + } + + #[test] + fn roundtrip_preserves_all_fields() { + let json = serde_json::json!({ + "resultType": "input_required", + "inputRequests": { + "key": { + "method": "elicitation/create", + "params": { + "message": "test", + "requestedSchema": { "type": "object", "properties": {} } + } + } + }, + "requestState": "abc123" + }); + + let result: InputRequiredResult = serde_json::from_value(json).unwrap(); + let reserialized = serde_json::to_value(&result).unwrap(); + + assert_eq!(reserialized["resultType"], "input_required"); + assert!(reserialized["inputRequests"].is_object()); + assert_eq!(reserialized["requestState"], "abc123"); + } + + #[test] + fn deserializes_with_request_state_only() { + let json = serde_json::json!({ + "resultType": "input_required", + "requestState": "eyJwcm9ncmVzcyI6IjUwJSJ9" + }); + + let result: InputRequiredResult = serde_json::from_value(json).unwrap(); + + assert!(result.input_requests.is_none()); + assert_eq!( + result.request_state.as_deref(), + Some("eyJwcm9ncmVzcyI6IjUwJSJ9") + ); + } + + #[test] + fn rejects_missing_result_type() { + let json = serde_json::json!({ + "requestState": "some-state" + }); + let err = serde_json::from_value::(json).unwrap_err(); + assert!( + err.to_string().contains("input_required"), + "error should mention the required resultType, got: {err}" + ); + } + + #[test] + fn rejects_wrong_result_type() { + let json = serde_json::json!({ + "resultType": "complete", + "requestState": "some-state" + }); + let err = serde_json::from_value::(json).unwrap_err(); + assert!( + err.to_string().contains("input_required"), + "error should mention the required resultType, got: {err}" + ); + } + } + + mod input_responses { + use super::*; + + #[test] + fn deserializes_heterogeneous_results() { + let json = serde_json::json!({ + "github_login": { + "action": "accept", + "content": { "name": "octocat" } + }, + "capital_of_france": { + "role": "assistant", + "content": { "type": "text", "text": "Paris." }, + "model": "claude-3-sonnet-20240307", + "stopReason": "endTurn" + } + }); + + let responses: InputResponses = serde_json::from_value(json).unwrap(); + + assert_eq!(responses.len(), 2); + assert!(responses.contains_key("github_login")); + assert!(responses.contains_key("capital_of_france")); + } + } + + mod constructors { + use super::*; + + #[test] + fn from_request_state_sets_state_only() { + let result = InputRequiredResult::from_request_state("opaque"); + + assert_eq!(result.result_type, ResultType::INPUT_REQUIRED); + assert!(result.input_requests.is_none()); + assert_eq!(result.request_state.as_deref(), Some("opaque")); + } + + #[test] + fn from_input_requests_sets_requests_only() { + let mut requests = InputRequests::new(); + requests.insert( + "key".to_string(), + serde_json::from_value(serde_json::json!({ + "method": "elicitation/create", + "params": { + "message": "test", + "requestedSchema": { "type": "object", "properties": {} } + } + })) + .unwrap(), + ); + + let result = InputRequiredResult::from_input_requests(requests); + + assert!(result.input_requests.is_some()); + assert!(result.request_state.is_none()); + } + } +} diff --git a/crates/rmcp/src/model/serde_impl.rs b/crates/rmcp/src/model/serde_impl.rs index f8996f318..7ff91099f 100644 --- a/crates/rmcp/src/model/serde_impl.rs +++ b/crates/rmcp/src/model/serde_impl.rs @@ -451,6 +451,8 @@ mod test { name: "my_tool".into(), arguments: None, task: None, + input_responses: None, + request_state: None, }, }; @@ -489,6 +491,8 @@ mod test { name: "my_tool".into(), arguments: None, task: None, + input_responses: None, + request_state: None, }, }; @@ -510,6 +514,8 @@ mod test { name: "my_tool".into(), arguments: None, task: None, + input_responses: None, + request_state: None, }, }; @@ -528,6 +534,8 @@ mod test { name: "my_tool".into(), arguments: None, task: None, + input_responses: None, + request_state: None, }, }; @@ -564,6 +572,8 @@ mod test { name: "my_tool".into(), arguments: None, task: None, + input_responses: None, + request_state: None, }, }; @@ -590,6 +600,8 @@ mod test { name: "my_tool".into(), arguments: Some(serde_json::Map::from_iter([("x".to_string(), json!(1))])), task: None, + input_responses: None, + request_state: None, }, }; diff --git a/crates/rmcp/src/service/server.rs b/crates/rmcp/src/service/server.rs index aa51e4704..01ee8784a 100644 --- a/crates/rmcp/src/service/server.rs +++ b/crates/rmcp/src/service/server.rs @@ -468,7 +468,13 @@ impl Peer { #[cfg(feature = "elicitation")] method!(peer_req_with_timeout create_elicitation_with_timeout CreateElicitationRequest(CreateElicitationRequestParams) => CreateElicitationResult); #[cfg(feature = "elicitation")] - method!(peer_not notify_url_elicitation_completed ElicitationCompletionNotification(ElicitationResponseNotificationParam)); + method!( + #[deprecated( + since = "2.0.0", + note = "URL elicitation is removed by SEP-2322 (Multi Round-Trip Requests). Use InputRequiredResult-based MRTR flow instead." + )] + peer_not notify_url_elicitation_completed ElicitationCompletionNotification(ElicitationResponseNotificationParam) + ); method!(peer_not notify_cancelled CancelledNotification(CancelledNotificationParam)); method!(peer_not notify_progress ProgressNotification(ProgressNotificationParam)); diff --git a/crates/rmcp/tests/test_elicitation.rs b/crates/rmcp/tests/test_elicitation.rs index 04a112f5b..1ceee0a09 100644 --- a/crates/rmcp/tests/test_elicitation.rs +++ b/crates/rmcp/tests/test_elicitation.rs @@ -2067,6 +2067,7 @@ async fn test_elicitation_both_modes() { /// Test URL_ELICITATION_REQUIRED error code #[tokio::test] +#[allow(deprecated)] async fn test_url_elicitation_required_error_code() { // Test the error code constant assert_eq!(ErrorCode::URL_ELICITATION_REQUIRED.0, -32042); diff --git a/crates/rmcp/tests/test_list_tools_result/list_tools_result.json b/crates/rmcp/tests/test_list_tools_result/list_tools_result.json index 15325e8fa..c6616b596 100644 --- a/crates/rmcp/tests/test_list_tools_result/list_tools_result.json +++ b/crates/rmcp/tests/test_list_tools_result/list_tools_result.json @@ -1,5 +1,6 @@ { "result": { + "resultType": "complete", "tools": [ { "name": "add", 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..dc92229ea 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 @@ -207,10 +207,25 @@ ], "additionalProperties": true }, + "inputResponses": { + "description": "Client responses to server-initiated input requests from a previous\n[`InputRequiredResult`]. Present only when retrying after an incomplete result.", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "name": { "description": "The name of the tool to call", "type": "string" }, + "requestState": { + "description": "Opaque request state echoed back from a previous [`InputRequiredResult`].\nClients MUST return this value exactly as received.", + "type": [ + "string", + "null" + ] + }, "task": { "description": "Task metadata for async task management (SEP-1319)", "type": [ @@ -649,8 +664,23 @@ ], "additionalProperties": true }, + "inputResponses": { + "description": "Client responses to server-initiated input requests from a previous\n[`InputRequiredResult`].", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "name": { "type": "string" + }, + "requestState": { + "description": "Opaque request state echoed back from a previous [`InputRequiredResult`].", + "type": [ + "string", + "null" + ] } }, "required": [ @@ -1369,6 +1399,21 @@ ], "additionalProperties": true }, + "inputResponses": { + "description": "Client responses to server-initiated input requests from a previous\n[`InputRequiredResult`].", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "requestState": { + "description": "Opaque request state echoed back from a previous [`InputRequiredResult`].", + "type": [ + "string", + "null" + ] + }, "uri": { "description": "The URI of the resource to read", "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..dc92229ea 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 @@ -207,10 +207,25 @@ ], "additionalProperties": true }, + "inputResponses": { + "description": "Client responses to server-initiated input requests from a previous\n[`InputRequiredResult`]. Present only when retrying after an incomplete result.", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "name": { "description": "The name of the tool to call", "type": "string" }, + "requestState": { + "description": "Opaque request state echoed back from a previous [`InputRequiredResult`].\nClients MUST return this value exactly as received.", + "type": [ + "string", + "null" + ] + }, "task": { "description": "Task metadata for async task management (SEP-1319)", "type": [ @@ -649,8 +664,23 @@ ], "additionalProperties": true }, + "inputResponses": { + "description": "Client responses to server-initiated input requests from a previous\n[`InputRequiredResult`].", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, "name": { "type": "string" + }, + "requestState": { + "description": "Opaque request state echoed back from a previous [`InputRequiredResult`].", + "type": [ + "string", + "null" + ] } }, "required": [ @@ -1369,6 +1399,21 @@ ], "additionalProperties": true }, + "inputResponses": { + "description": "Client responses to server-initiated input requests from a previous\n[`InputRequiredResult`].", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "requestState": { + "description": "Opaque request state echoed back from a previous [`InputRequiredResult`].", + "type": [ + "string", + "null" + ] + }, "uri": { "description": "The URI of the resource to read", "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 6de03e31f..6a5314a39 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 @@ -371,6 +371,15 @@ "null" ] }, + "resultType": { + "description": "Result type discriminator. Always serialized; older peers ignore it.", + "allOf": [ + { + "$ref": "#/definitions/ResultType" + } + ], + "default": "complete" + }, "structuredContent": { "description": "An optional JSON object that represents the structured result of the tool call" } @@ -467,6 +476,15 @@ "properties": { "completion": { "$ref": "#/definitions/CompletionInfo" + }, + "resultType": { + "description": "Result type discriminator. Always serialized; older peers ignore it.", + "allOf": [ + { + "$ref": "#/definitions/ResultType" + } + ], + "default": "complete" } }, "required": [ @@ -972,6 +990,15 @@ "items": { "$ref": "#/definitions/PromptMessage" } + }, + "resultType": { + "description": "Result type discriminator. Always serialized; older peers ignore it.", + "allOf": [ + { + "$ref": "#/definitions/ResultType" + } + ], + "default": "complete" } }, "required": [ @@ -1185,6 +1212,77 @@ "serverInfo" ] }, + "InputRequest": { + "description": "A server-initiated request that can appear inside [`InputRequests`].\n\nPer the MCP spec, only `CreateMessageRequest` (sampling),\n`CreateElicitationRequest` (elicitation), and `ListRootsRequest` (roots)\nare allowed. This is modeled as an untagged enum rather than a\n`ServerRequest` alias to prevent `PingRequest` or `CustomRequest` from\nbeing included.", + "anyOf": [ + { + "description": "A `sampling/createMessage` request.", + "allOf": [ + { + "$ref": "#/definitions/Request" + } + ] + }, + { + "description": "An `elicitation/create` request.", + "allOf": [ + { + "$ref": "#/definitions/Request2" + } + ] + }, + { + "description": "A `roots/list` request.", + "allOf": [ + { + "$ref": "#/definitions/RequestNoParam2" + } + ] + } + ] + }, + "InputRequiredResult": { + "description": "A result indicating that additional input is needed before the request\ncan be completed.\n\nAt least one of [`input_requests`](Self::input_requests) or\n[`request_state`](Self::request_state) MUST be present.\n\nServers MAY send this in response to `tools/call`, `prompts/get`, or\n`resources/read`. Servers MUST NOT send this for any other request.\n\n# Examples\n\n```\nuse rmcp::model::InputRequiredResult;\n\nlet result = InputRequiredResult::from_request_state(\"opaque-server-state\");\nassert!(result.input_requests.is_none());\nassert_eq!(result.request_state.as_deref(), Some(\"opaque-server-state\"));\n```", + "type": "object", + "properties": { + "_meta": { + "description": "Optional protocol-level metadata.", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "inputRequests": { + "description": "Server-initiated requests that the client must fulfill before retrying.", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "#/definitions/InputRequest" + } + }, + "requestState": { + "description": "Opaque request state to be echoed back by the client on retry.\nClients MUST NOT inspect, parse, modify, or make any assumptions\nabout the contents.", + "type": [ + "string", + "null" + ] + }, + "resultType": { + "description": "Always `\"input_required\"` for this result type.", + "allOf": [ + { + "$ref": "#/definitions/ResultType" + } + ] + } + }, + "required": [ + "resultType" + ] + }, "IntegerSchema": { "description": "Schema definition for integer properties.\n\nCompliant with MCP 2025-06-18 specification for elicitation schemas.\nSupports only the fields allowed by the MCP spec.", "type": "object", @@ -1427,6 +1525,15 @@ "items": { "$ref": "#/definitions/Prompt" } + }, + "resultType": { + "description": "Result type discriminator. Always serialized; older peers ignore it.", + "allOf": [ + { + "$ref": "#/definitions/ResultType" + } + ], + "default": "complete" } }, "required": [ @@ -1454,6 +1561,15 @@ "items": { "$ref": "#/definitions/Annotated3" } + }, + "resultType": { + "description": "Result type discriminator. Always serialized; older peers ignore it.", + "allOf": [ + { + "$ref": "#/definitions/ResultType" + } + ], + "default": "complete" } }, "required": [ @@ -1481,6 +1597,15 @@ "items": { "$ref": "#/definitions/Annotated2" } + }, + "resultType": { + "description": "Result type discriminator. Always serialized; older peers ignore it.", + "allOf": [ + { + "$ref": "#/definitions/ResultType" + } + ], + "default": "complete" } }, "required": [ @@ -1536,6 +1661,15 @@ "null" ] }, + "resultType": { + "description": "Result type discriminator. Always serialized; older peers ignore it.", + "allOf": [ + { + "$ref": "#/definitions/ResultType" + } + ], + "default": "complete" + }, "tools": { "type": "array", "items": { @@ -2409,6 +2543,15 @@ "items": { "$ref": "#/definitions/ResourceContents" } + }, + "result_type": { + "description": "Result type discriminator. Always serialized; older peers ignore it.", + "allOf": [ + { + "$ref": "#/definitions/ResultType" + } + ], + "default": "complete" } }, "required": [ @@ -2569,6 +2712,10 @@ } } }, + "ResultType": { + "description": "Indicates the type of a result object, allowing the client to\ndetermine how to parse the response.\n\nThe spec defines this as an open string (`\"complete\" | \"input_required\" | string`),\nso unknown values are preserved rather than rejected. Servers implementing this\nprotocol version MUST include `resultType` in every result. For backward\ncompatibility, clients MUST treat an absent field as `\"complete\"`.", + "type": "string" + }, "Role": { "description": "Represents the role of a participant in a conversation or message exchange.\n\nUsed in sampling and chat contexts to distinguish between different\ntypes of message senders in the conversation flow.", "oneOf": [ @@ -2863,6 +3010,9 @@ { "$ref": "#/definitions/CallToolResult" }, + { + "$ref": "#/definitions/InputRequiredResult" + }, { "$ref": "#/definitions/GetTaskPayloadResult" }, 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 6de03e31f..6a5314a39 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 @@ -371,6 +371,15 @@ "null" ] }, + "resultType": { + "description": "Result type discriminator. Always serialized; older peers ignore it.", + "allOf": [ + { + "$ref": "#/definitions/ResultType" + } + ], + "default": "complete" + }, "structuredContent": { "description": "An optional JSON object that represents the structured result of the tool call" } @@ -467,6 +476,15 @@ "properties": { "completion": { "$ref": "#/definitions/CompletionInfo" + }, + "resultType": { + "description": "Result type discriminator. Always serialized; older peers ignore it.", + "allOf": [ + { + "$ref": "#/definitions/ResultType" + } + ], + "default": "complete" } }, "required": [ @@ -972,6 +990,15 @@ "items": { "$ref": "#/definitions/PromptMessage" } + }, + "resultType": { + "description": "Result type discriminator. Always serialized; older peers ignore it.", + "allOf": [ + { + "$ref": "#/definitions/ResultType" + } + ], + "default": "complete" } }, "required": [ @@ -1185,6 +1212,77 @@ "serverInfo" ] }, + "InputRequest": { + "description": "A server-initiated request that can appear inside [`InputRequests`].\n\nPer the MCP spec, only `CreateMessageRequest` (sampling),\n`CreateElicitationRequest` (elicitation), and `ListRootsRequest` (roots)\nare allowed. This is modeled as an untagged enum rather than a\n`ServerRequest` alias to prevent `PingRequest` or `CustomRequest` from\nbeing included.", + "anyOf": [ + { + "description": "A `sampling/createMessage` request.", + "allOf": [ + { + "$ref": "#/definitions/Request" + } + ] + }, + { + "description": "An `elicitation/create` request.", + "allOf": [ + { + "$ref": "#/definitions/Request2" + } + ] + }, + { + "description": "A `roots/list` request.", + "allOf": [ + { + "$ref": "#/definitions/RequestNoParam2" + } + ] + } + ] + }, + "InputRequiredResult": { + "description": "A result indicating that additional input is needed before the request\ncan be completed.\n\nAt least one of [`input_requests`](Self::input_requests) or\n[`request_state`](Self::request_state) MUST be present.\n\nServers MAY send this in response to `tools/call`, `prompts/get`, or\n`resources/read`. Servers MUST NOT send this for any other request.\n\n# Examples\n\n```\nuse rmcp::model::InputRequiredResult;\n\nlet result = InputRequiredResult::from_request_state(\"opaque-server-state\");\nassert!(result.input_requests.is_none());\nassert_eq!(result.request_state.as_deref(), Some(\"opaque-server-state\"));\n```", + "type": "object", + "properties": { + "_meta": { + "description": "Optional protocol-level metadata.", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "inputRequests": { + "description": "Server-initiated requests that the client must fulfill before retrying.", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "#/definitions/InputRequest" + } + }, + "requestState": { + "description": "Opaque request state to be echoed back by the client on retry.\nClients MUST NOT inspect, parse, modify, or make any assumptions\nabout the contents.", + "type": [ + "string", + "null" + ] + }, + "resultType": { + "description": "Always `\"input_required\"` for this result type.", + "allOf": [ + { + "$ref": "#/definitions/ResultType" + } + ] + } + }, + "required": [ + "resultType" + ] + }, "IntegerSchema": { "description": "Schema definition for integer properties.\n\nCompliant with MCP 2025-06-18 specification for elicitation schemas.\nSupports only the fields allowed by the MCP spec.", "type": "object", @@ -1427,6 +1525,15 @@ "items": { "$ref": "#/definitions/Prompt" } + }, + "resultType": { + "description": "Result type discriminator. Always serialized; older peers ignore it.", + "allOf": [ + { + "$ref": "#/definitions/ResultType" + } + ], + "default": "complete" } }, "required": [ @@ -1454,6 +1561,15 @@ "items": { "$ref": "#/definitions/Annotated3" } + }, + "resultType": { + "description": "Result type discriminator. Always serialized; older peers ignore it.", + "allOf": [ + { + "$ref": "#/definitions/ResultType" + } + ], + "default": "complete" } }, "required": [ @@ -1481,6 +1597,15 @@ "items": { "$ref": "#/definitions/Annotated2" } + }, + "resultType": { + "description": "Result type discriminator. Always serialized; older peers ignore it.", + "allOf": [ + { + "$ref": "#/definitions/ResultType" + } + ], + "default": "complete" } }, "required": [ @@ -1536,6 +1661,15 @@ "null" ] }, + "resultType": { + "description": "Result type discriminator. Always serialized; older peers ignore it.", + "allOf": [ + { + "$ref": "#/definitions/ResultType" + } + ], + "default": "complete" + }, "tools": { "type": "array", "items": { @@ -2409,6 +2543,15 @@ "items": { "$ref": "#/definitions/ResourceContents" } + }, + "result_type": { + "description": "Result type discriminator. Always serialized; older peers ignore it.", + "allOf": [ + { + "$ref": "#/definitions/ResultType" + } + ], + "default": "complete" } }, "required": [ @@ -2569,6 +2712,10 @@ } } }, + "ResultType": { + "description": "Indicates the type of a result object, allowing the client to\ndetermine how to parse the response.\n\nThe spec defines this as an open string (`\"complete\" | \"input_required\" | string`),\nso unknown values are preserved rather than rejected. Servers implementing this\nprotocol version MUST include `resultType` in every result. For backward\ncompatibility, clients MUST treat an absent field as `\"complete\"`.", + "type": "string" + }, "Role": { "description": "Represents the role of a participant in a conversation or message exchange.\n\nUsed in sampling and chat contexts to distinguish between different\ntypes of message senders in the conversation flow.", "oneOf": [ @@ -2863,6 +3010,9 @@ { "$ref": "#/definitions/CallToolResult" }, + { + "$ref": "#/definitions/InputRequiredResult" + }, { "$ref": "#/definitions/GetTaskPayloadResult" }, diff --git a/crates/rmcp/tests/test_tool_result_meta.rs b/crates/rmcp/tests/test_tool_result_meta.rs index f64d8e3f1..28640bbf1 100644 --- a/crates/rmcp/tests/test_tool_result_meta.rs +++ b/crates/rmcp/tests/test_tool_result_meta.rs @@ -9,6 +9,7 @@ fn serialize_tool_result_with_meta() { let result = CallToolResult::success(content).with_meta(Some(meta)); let v = serde_json::to_value(&result).unwrap(); let expected = json!({ + "resultType": "complete", "content": [{"type":"text","text":"ok"}], "isError": false, "_meta": {"foo":"bar"} diff --git a/examples/servers/src/common/counter.rs b/examples/servers/src/common/counter.rs index d78acd59f..96fb9857d 100644 --- a/examples/servers/src/common/counter.rs +++ b/examples/servers/src/common/counter.rs @@ -255,8 +255,7 @@ impl ServerHandler for Counter { self._create_resource_text("str:////Users/to/some/path/", "cwd"), self._create_resource_text("memo://insights", "memo-name"), ], - next_cursor: None, - meta: None, + ..Default::default() }) } @@ -296,9 +295,8 @@ impl ServerHandler for Counter { _: RequestContext, ) -> Result { Ok(ListResourceTemplatesResult { - next_cursor: None, resource_templates: Vec::new(), - meta: None, + ..Default::default() }) } diff --git a/examples/servers/src/sampling_stdio.rs b/examples/servers/src/sampling_stdio.rs index 9c1d21d6d..244a4aa5c 100644 --- a/examples/servers/src/sampling_stdio.rs +++ b/examples/servers/src/sampling_stdio.rs @@ -113,8 +113,7 @@ impl ServerHandler for SamplingDemoServer { .unwrap(), ), )], - meta: None, - next_cursor: None, + ..Default::default() }) } }