feat!: add MRTR model types (SEP-2322) (#915)

* feat!: add SEP-2322 MRTR model types

* feat!: remove URLElicitationRequiredError (SEP-2322)
This commit is contained in:
Dale Seo 2026-07-08 14:03:33 -04:00 committed by GitHub
parent 519577601d
commit f4ff56b81d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 969 additions and 255 deletions

View file

@ -204,9 +204,8 @@ impl ServerHandler for ConformanceServer {
),
];
Ok(ListToolsResult {
meta: None,
tools,
next_cursor: None,
..Default::default()
})
}
@ -540,7 +539,6 @@ impl ServerHandler for ConformanceServer {
_cx: RequestContext<RoleServer>,
) -> Result<ListResourcesResult, ErrorData> {
Ok(ListResourcesResult {
meta: None,
resources: vec![
Resource::new("test://static-text", "Static Text Resource")
.with_description("A static text resource for testing")
@ -549,7 +547,7 @@ impl ServerHandler for ConformanceServer {
.with_description("A static binary/blob resource for testing")
.with_mime_type("image/png"),
],
next_cursor: None,
..Default::default()
})
}
@ -609,13 +607,12 @@ impl ServerHandler for ConformanceServer {
_cx: RequestContext<RoleServer>,
) -> Result<ListResourceTemplatesResult, ErrorData> {
Ok(ListResourceTemplatesResult {
meta: None,
resource_templates: vec![
ResourceTemplate::new("test://template/{id}/data", "Dynamic Resource")
.with_description("A dynamic resource with parameter substitution")
.with_mime_type("application/json"),
],
next_cursor: None,
..Default::default()
})
}
@ -645,7 +642,6 @@ impl ServerHandler for ConformanceServer {
_cx: RequestContext<RoleServer>,
) -> Result<ListPromptsResult, ErrorData> {
Ok(ListPromptsResult {
meta: None,
prompts: vec![
Prompt::new(
"test_simple_prompt",
@ -675,7 +671,7 @@ impl ServerHandler for ConformanceServer {
None,
),
],
next_cursor: None,
..Default::default()
})
}

View file

@ -61,6 +61,7 @@ pub fn prompt_handler(attr: TokenStream, input: TokenStream) -> syn::Result<Toke
) -> Result<rmcp::model::ListPromptsResult, rmcp::ErrorData> {
let prompts = #router_expr.list_all();
Ok(rmcp::model::ListPromptsResult {
result_type: Default::default(),
prompts,
meta: #meta,
next_cursor: None,

View file

@ -69,6 +69,7 @@ pub fn tool_handler(attr: TokenStream, input: TokenStream) -> syn::Result<TokenS
_context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<rmcp::model::ListToolsResult, rmcp::ErrorData> {
Ok(rmcp::model::ListToolsResult{
result_type: Default::default(),
tools: #router.list_all(),
meta: #result_meta,
next_cursor: None,

View file

@ -66,10 +66,6 @@ impl<H: ClientHandler> Service<RoleClient> for H {
ServerNotification::PromptListChangedNotification(_notification_no_param) => {
self.on_prompt_list_changed(context).await
}
ServerNotification::ElicitationCompleteNotification(notification) => {
self.on_url_elicitation_notification_complete(notification.params, context)
.await
}
ServerNotification::TaskStatusNotification(notification) => {
self.on_task_status(notification.params, context).await
}
@ -242,13 +238,6 @@ pub trait ClientHandler: Sized + Send + Sync + 'static {
std::future::ready(())
}
fn on_url_elicitation_notification_complete(
&self,
params: ElicitationResponseNotificationParam,
context: NotificationContext<RoleClient>,
) -> impl Future<Output = ()> + MaybeSendFuture + '_ {
std::future::ready(())
}
fn on_task_status(
&self,
params: TaskStatusNotificationParam,

View file

@ -103,6 +103,7 @@ impl IntoGetPromptResult for GetPromptResult {
impl IntoGetPromptResult for Vec<PromptMessage> {
fn into_get_prompt_result(self) -> Result<GetPromptResult, crate::ErrorData> {
Ok(GetPromptResult {
result_type: Default::default(),
description: None,
messages: self,
meta: None,

View file

@ -669,6 +669,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),
);
@ -708,6 +710,8 @@ mod tests {
name: Cow::Borrowed("test_tool"),
arguments: None,
task: None,
input_responses: None,
request_state: None,
},
RequestContext::new(NumberOrString::Number(1), peer),
);

View file

@ -46,6 +46,7 @@ impl<'s, S> ToolCallContext<'s, S> {
name,
arguments,
task,
..
}: CallToolRequestParams,
request_context: RequestContext<RoleServer>,
) -> Self {

View file

@ -12,6 +12,7 @@ mod content;
mod elicitation_schema;
mod extension;
mod meta;
mod mrtr;
mod prompt;
mod resource;
mod serde_impl;
@ -23,6 +24,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};
@ -516,7 +518,6 @@ impl ErrorCode {
pub const INVALID_PARAMS: Self = Self(-32602);
pub const INTERNAL_ERROR: Self = Self(-32603);
pub const PARSE_ERROR: Self = Self(-32700);
pub const URL_ELICITATION_REQUIRED: Self = Self(-32042);
}
/// Error information for JSON-RPC error responses.
@ -572,12 +573,6 @@ impl ErrorData {
pub fn internal_error(message: impl Into<Cow<'static, str>>, data: Option<Value>) -> Self {
Self::new(ErrorCode::INTERNAL_ERROR, message, data)
}
pub fn url_elicitation_required(
message: impl Into<Cow<'static, str>>,
data: Option<Value>,
) -> Self {
Self::new(ErrorCode::URL_ELICITATION_REQUIRED, message, data)
}
}
/// Represents any JSON-RPC message that can be sent or received.
@ -684,6 +679,71 @@ impl From<EmptyResult> 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.0.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for ResultType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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)]
@ -1185,6 +1245,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. Absent values deserialize as `"complete"`.
#[serde(default)]
pub result_type: ResultType,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<Meta>,
#[serde(skip_serializing_if = "Option::is_none")]
@ -1197,6 +1260,7 @@ macro_rules! paginated_result {
items: $t_item,
) -> Self {
Self {
result_type: ResultType::default(),
meta: None,
next_cursor: None,
$i_item: items,
@ -1240,6 +1304,13 @@ pub struct ReadResourceRequestParams {
pub meta: Option<Meta>,
/// 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<InputResponses>,
/// Opaque request state echoed back from a previous [`InputRequiredResult`].
#[serde(skip_serializing_if = "Option::is_none")]
pub request_state: Option<String>,
}
impl ReadResourceRequestParams {
@ -1248,6 +1319,8 @@ impl ReadResourceRequestParams {
Self {
meta: None,
uri: uri.into(),
input_responses: None,
request_state: None,
}
}
@ -1256,6 +1329,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<String>) -> Self {
self.request_state = Some(request_state.into());
self
}
}
impl RequestParamsMeta for ReadResourceRequestParams {
@ -1276,6 +1361,9 @@ pub type ReadResourceRequestParam = ReadResourceRequestParams;
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct ReadResourceResult {
/// Result type discriminator. Absent values deserialize as `"complete"`.
#[serde(default)]
pub result_type: ResultType,
/// The actual content of the resource
pub contents: Vec<ResourceContents>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
@ -1286,6 +1374,7 @@ impl ReadResourceResult {
/// Create a new ReadResourceResult with the given contents.
pub fn new(contents: Vec<ResourceContents>) -> Self {
Self {
result_type: ResultType::default(),
contents,
meta: None,
}
@ -1433,6 +1522,13 @@ pub struct GetPromptRequestParams {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<JsonObject>,
/// Client responses to server-initiated input requests from a previous
/// [`InputRequiredResult`].
#[serde(skip_serializing_if = "Option::is_none")]
pub input_responses: Option<InputResponses>,
/// Opaque request state echoed back from a previous [`InputRequiredResult`].
#[serde(skip_serializing_if = "Option::is_none")]
pub request_state: Option<String>,
}
impl GetPromptRequestParams {
@ -1442,6 +1538,8 @@ impl GetPromptRequestParams {
meta: None,
name: name.into(),
arguments: None,
input_responses: None,
request_state: None,
}
}
@ -1456,6 +1554,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<String>) -> Self {
self.request_state = Some(request_state.into());
self
}
}
impl RequestParamsMeta for GetPromptRequestParams {
@ -2438,6 +2548,9 @@ impl CompletionInfo {
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct CompleteResult {
/// Result type discriminator. Absent values deserialize as `"complete"`.
#[serde(default)]
pub result_type: ResultType,
pub completion: CompletionInfo,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<Meta>,
@ -2447,6 +2560,7 @@ impl CompleteResult {
/// Create a new CompleteResult with the given completion info.
pub fn new(completion: CompletionInfo) -> Self {
Self {
result_type: ResultType::default(),
completion,
meta: None,
}
@ -2653,7 +2767,6 @@ pub type RootsListChangedNotification = NotificationNoParam<RootsListChangedNoti
// Elicitation allows servers to request interactive input from users during tool execution.
const_string!(ElicitationCreateRequestMethod = "elicitation/create");
const_string!(ElicitationResponseNotificationMethod = "notifications/elicitation/response");
const_string!(ElicitationCompletionNotificationMethod = "notifications/elicitation/complete");
/// Represents the possible actions a user can take in response to an elicitation request.
///
@ -2888,34 +3001,6 @@ pub type ElicitRequest = Request<ElicitationCreateRequestMethod, ElicitRequestPa
#[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))]
#[non_exhaustive]
pub struct ElicitationResponseNotificationParam {
pub elicitation_id: String,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<Meta>,
}
impl ElicitationResponseNotificationParam {
/// Create a new ElicitationResponseNotificationParam.
pub fn new(elicitation_id: impl Into<String>) -> Self {
Self {
elicitation_id: elicitation_id.into(),
meta: None,
}
}
}
/// Notification sent when an url elicitation process is completed.
pub type ElicitationCompleteNotification =
Notification<ElicitationCompletionNotificationMethod, ElicitationResponseNotificationParam>;
#[deprecated(since = "2.0.0", note = "Renamed to ElicitationCompleteNotification")]
pub type ElicitationCompletionNotification = ElicitationCompleteNotification;
// =============================================================================
// TOOL EXECUTION RESULTS
// =============================================================================
@ -2929,6 +3014,9 @@ pub type ElicitationCompletionNotification = ElicitationCompleteNotification;
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct CallToolResult {
/// Result type discriminator. Absent values deserialize as `"complete"`.
#[serde(default)]
pub result_type: ResultType,
/// The content returned by the tool (text, images, etc.)
#[serde(default)]
pub content: Vec<ContentBlock>,
@ -2956,6 +3044,8 @@ impl<'de> Deserialize<'de> for CallToolResult {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Helper {
#[serde(default)]
result_type: ResultType,
content: Option<Vec<ContentBlock>>,
structured_content: Option<Value>,
is_error: Option<bool>,
@ -2977,6 +3067,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,
@ -2989,6 +3080,7 @@ impl CallToolResult {
/// Create a successful tool result with unstructured content
pub fn success(content: Vec<ContentBlock>) -> Self {
CallToolResult {
result_type: ResultType::default(),
content,
structured_content: None,
is_error: Some(false),
@ -3046,6 +3138,7 @@ impl CallToolResult {
/// ```
pub fn error(content: Vec<ContentBlock>) -> Self {
CallToolResult {
result_type: ResultType::default(),
content,
structured_content: None,
is_error: Some(true),
@ -3068,6 +3161,7 @@ impl CallToolResult {
/// ```
pub fn structured(value: Value) -> Self {
CallToolResult {
result_type: ResultType::default(),
content: vec![ContentBlock::text(value.to_string())],
structured_content: Some(value),
is_error: Some(false),
@ -3094,6 +3188,7 @@ impl CallToolResult {
/// ```
pub fn structured_error(value: Value) -> Self {
CallToolResult {
result_type: ResultType::default(),
content: vec![ContentBlock::text(value.to_string())],
structured_content: Some(value),
is_error: Some(true),
@ -3170,6 +3265,14 @@ pub struct CallToolRequestParams {
/// Task metadata for async task management (SEP-1319)
#[serde(skip_serializing_if = "Option::is_none")]
pub task: Option<TaskMetadata>,
/// 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<InputResponses>,
/// 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<String>,
}
impl CallToolRequestParams {
@ -3180,6 +3283,8 @@ impl CallToolRequestParams {
name: name.into(),
arguments: None,
task: None,
input_responses: None,
request_state: None,
}
}
@ -3194,6 +3299,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<String>) -> Self {
self.request_state = Some(request_state.into());
self
}
}
impl RequestParamsMeta for CallToolRequestParams {
@ -3286,6 +3403,9 @@ impl CreateMessageResult {
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct GetPromptResult {
/// Result type discriminator. Absent values deserialize as `"complete"`.
#[serde(default)]
pub result_type: ResultType,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub messages: Vec<PromptMessage>,
@ -3297,6 +3417,7 @@ impl GetPromptResult {
/// Create a new GetPromptResult with required fields.
pub fn new(messages: Vec<PromptMessage>) -> Self {
Self {
result_type: ResultType::default(),
description: None,
messages,
meta: None,
@ -3662,7 +3783,6 @@ ts_union!(
| ResourceListChangedNotification
| ToolListChangedNotification
| PromptListChangedNotification
| ElicitationCompleteNotification
| TaskStatusNotification
| CustomNotification;
);
@ -3683,6 +3803,7 @@ ts_union!(
| GetTaskResult
| CancelTaskResult
| CallToolResult
| InputRequiredResult
| GetTaskPayloadResult
| EmptyResult
| CustomResult

View file

@ -218,7 +218,6 @@ variant_extension! {
ResourceListChangedNotification
ToolListChangedNotification
PromptListChangedNotification
ElicitationCompleteNotification
TaskStatusNotification
CustomNotification
}

View file

@ -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::{CreateMessageRequest, ElicitRequest, ListRootsRequest, Meta, ResultType};
/// A server-initiated request that can appear inside [`InputRequests`].
///
/// Per the MCP spec, only `CreateMessageRequest` (sampling),
/// `ElicitRequest` (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(ElicitRequest),
/// 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<String, InputRequest>;
/// 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<String, Value>;
/// 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<InputRequests>,
/// 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<String>,
/// Optional protocol-level metadata.
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<Meta>,
}
/// 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<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Helper {
result_type: Option<ResultType>,
input_requests: Option<InputRequests>,
request_state: Option<String>,
#[serde(rename = "_meta")]
meta: Option<Meta>,
}
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<InputRequests>, request_state: Option<String>) -> 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<String>) -> 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::<InputRequiredResult>(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::<InputRequiredResult>(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());
}
}
}

View file

@ -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,
},
};

View file

@ -10,10 +10,7 @@ use url::Url;
use super::*;
#[cfg(feature = "elicitation")]
use crate::model::{
ElicitRequest, ElicitRequestParams, ElicitResult, ElicitationAction,
ElicitationCompleteNotification, ElicitationResponseNotificationParam,
};
use crate::model::{ElicitRequest, ElicitRequestParams, ElicitResult, ElicitationAction};
use crate::{
model::{
CancelledNotification, CancelledNotificationParam, ClientInfo, ClientJsonRpcMessage,
@ -474,8 +471,6 @@ impl Peer<RoleServer> {
method!(peer_req create_elicitation ElicitRequest(ElicitRequestParams) => ElicitResult);
#[cfg(feature = "elicitation")]
method!(peer_req_with_timeout create_elicitation_with_timeout ElicitRequest(ElicitRequestParams) => ElicitResult);
#[cfg(feature = "elicitation")]
method!(peer_not notify_url_elicitation_completed ElicitationCompleteNotification(ElicitationResponseNotificationParam));
method!(peer_not notify_cancelled CancelledNotification(CancelledNotificationParam));
method!(peer_not notify_progress ProgressNotification(ProgressNotificationParam));

View file

@ -1882,31 +1882,6 @@ async fn test_url_elicitation_json_rpc_protocol() {
}
}
/// Test ElicitationCompleteNotification serialization/deserialization
#[tokio::test]
async fn test_elicitation_completion_notification() {
let notification_params = ElicitationResponseNotificationParam::new("elicit-789");
// Test serialization
let json = serde_json::to_value(&notification_params).unwrap();
let expected = json!({
"elicitationId": "elicit-789"
});
assert_eq!(json, expected);
// Test deserialization
let deserialized: ElicitationResponseNotificationParam =
serde_json::from_value(expected).unwrap();
assert_eq!(deserialized.elicitation_id, "elicit-789");
// Test complete notification structure
let notification = ElicitationCompleteNotification::new(notification_params);
let json = serde_json::to_value(&notification).unwrap();
assert_eq!(json["method"], "notifications/elicitation/complete");
assert_eq!(json["params"]["elicitationId"], "elicit-789");
}
/// Test UrlElicitationCapability structure and serialization
#[tokio::test]
async fn test_url_elicitation_capability() {
@ -2020,39 +1995,6 @@ async fn test_elicitation_both_modes() {
assert!(url_json.get("requestedSchema").is_none());
}
/// Test URL_ELICITATION_REQUIRED error code
#[tokio::test]
async fn test_url_elicitation_required_error_code() {
// Test the error code constant
assert_eq!(ErrorCode::URL_ELICITATION_REQUIRED.0, -32042);
// Test creating error data with URL_ELICITATION_REQUIRED
let error_data = ErrorData::url_elicitation_required(
"URL elicitation is required for this operation",
Some(json!({
"url": "https://example.com/complete",
"elicitationId": "elicit-999"
})),
);
assert_eq!(error_data.code, ErrorCode::URL_ELICITATION_REQUIRED);
assert_eq!(
error_data.message,
"URL elicitation is required for this operation"
);
assert!(error_data.data.is_some());
// Test serialization
let json = serde_json::to_value(&error_data).unwrap();
assert_eq!(json["code"], -32042);
assert_eq!(
json["message"],
"URL elicitation is required for this operation"
);
assert_eq!(json["data"]["url"], "https://example.com/complete");
assert_eq!(json["data"]["elicitationId"], "elicit-999");
}
/// Test ClientCapabilities with different elicitation mode combinations
#[tokio::test]
async fn test_client_capabilities_elicitation_modes() {
@ -2102,32 +2044,6 @@ async fn test_client_capabilities_elicitation_modes() {
assert!(json["elicitation"]["url"].is_object());
}
/// Test ElicitationCompleteNotification in ServerNotification enum
#[tokio::test]
async fn test_elicitation_completion_in_server_notification() {
let notification_param = ElicitationResponseNotificationParam::new("notify-123");
let completion_notification = ElicitationCompleteNotification::new(notification_param.clone());
// Test that it's part of ServerNotification
let server_notification =
ServerNotification::ElicitationCompleteNotification(completion_notification);
// Test serialization
let json = serde_json::to_value(&server_notification).unwrap();
assert_eq!(json["method"], "notifications/elicitation/complete");
assert_eq!(json["params"]["elicitationId"], "notify-123");
// Test deserialization
let deserialized: ServerNotification = serde_json::from_value(json).unwrap();
match deserialized {
ServerNotification::ElicitationCompleteNotification(notif) => {
assert_eq!(notif.params.elicitation_id, "notify-123");
}
_ => panic!("Expected ElicitationCompleteNotification variant"),
}
}
/// Test ElicitationAction with URL elicitation workflow
#[tokio::test]
async fn test_url_elicitation_action_workflow() {
@ -2161,10 +2077,4 @@ async fn test_elicitation_method_constants() {
ElicitationResponseNotificationMethod::VALUE,
"notifications/elicitation/response"
);
// Test new completion notification method
assert_eq!(
ElicitationCompletionNotificationMethod::VALUE,
"notifications/elicitation/complete"
);
}

View file

@ -1,5 +1,6 @@
{
"result": {
"resultType": "complete",
"tools": [
{
"name": "add",

View file

@ -143,10 +143,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)",
"anyOf": [
@ -726,8 +741,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": [
@ -1374,6 +1404,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"

View file

@ -143,10 +143,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)",
"anyOf": [
@ -726,8 +741,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": [
@ -1374,6 +1404,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"

View file

@ -178,6 +178,15 @@
"null"
]
},
"resultType": {
"description": "Result type discriminator. Absent values deserialize as `\"complete\"`.",
"allOf": [
{
"$ref": "#/definitions/ResultType"
}
],
"default": "complete"
},
"structuredContent": {
"description": "An optional JSON object that represents the structured result of the tool call"
}
@ -292,6 +301,15 @@
},
"completion": {
"$ref": "#/definitions/CompletionInfo"
},
"resultType": {
"description": "Result type discriminator. Absent values deserialize as `\"complete\"`.",
"allOf": [
{
"$ref": "#/definitions/ResultType"
}
],
"default": "complete"
}
},
"required": [
@ -752,35 +770,11 @@
}
]
},
"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"
}
},
"required": [
"elicitationId"
]
},
"ElicitationSchema": {
"description": "Type-safe elicitation schema for requesting structured user input.\n\nThis enforces the MCP 2025-06-18 specification that elicitation schemas\nmust be objects with primitive-typed properties.\n\n# Example\n\n```rust\nuse rmcp::model::*;\n\nlet schema = ElicitationSchema::builder()\n .required_email(\"email\")\n .required_integer(\"age\", 0, 150)\n .optional_bool(\"newsletter\", false)\n .build();\n```",
"type": "object",
@ -948,6 +942,15 @@
"items": {
"$ref": "#/definitions/PromptMessage"
}
},
"resultType": {
"description": "Result type discriminator. Absent values deserialize as `\"complete\"`.",
"allOf": [
{
"$ref": "#/definitions/ResultType"
}
],
"default": "complete"
}
},
"required": [
@ -1205,6 +1208,77 @@
"serverInfo"
]
},
"InputRequest": {
"description": "A server-initiated request that can appear inside [`InputRequests`].\n\nPer the MCP spec, only `CreateMessageRequest` (sampling),\n`ElicitRequest` (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",
@ -1322,9 +1396,6 @@
{
"$ref": "#/definitions/Notification5"
},
{
"$ref": "#/definitions/Notification6"
},
{
"$ref": "#/definitions/CustomNotification"
}
@ -1456,6 +1527,15 @@
"items": {
"$ref": "#/definitions/Prompt"
}
},
"resultType": {
"description": "Result type discriminator. Absent values deserialize as `\"complete\"`.",
"allOf": [
{
"$ref": "#/definitions/ResultType"
}
],
"default": "complete"
}
},
"required": [
@ -1483,6 +1563,15 @@
"items": {
"$ref": "#/definitions/ResourceTemplate"
}
},
"resultType": {
"description": "Result type discriminator. Absent values deserialize as `\"complete\"`.",
"allOf": [
{
"$ref": "#/definitions/ResultType"
}
],
"default": "complete"
}
},
"required": [
@ -1510,6 +1599,15 @@
"items": {
"$ref": "#/definitions/Resource"
}
},
"resultType": {
"description": "Result type discriminator. Absent values deserialize as `\"complete\"`.",
"allOf": [
{
"$ref": "#/definitions/ResultType"
}
],
"default": "complete"
}
},
"required": [
@ -1564,6 +1662,15 @@
"null"
]
},
"resultType": {
"description": "Result type discriminator. Absent values deserialize as `\"complete\"`.",
"allOf": [
{
"$ref": "#/definitions/ResultType"
}
],
"default": "complete"
},
"tools": {
"type": "array",
"items": {
@ -1758,21 +1865,6 @@
]
},
"Notification5": {
"type": "object",
"properties": {
"method": {
"$ref": "#/definitions/ElicitationCompletionNotificationMethod"
},
"params": {
"$ref": "#/definitions/ElicitationResponseNotificationParam"
}
},
"required": [
"method",
"params"
]
},
"Notification6": {
"type": "object",
"properties": {
"method": {
@ -2129,6 +2221,15 @@
"items": {
"$ref": "#/definitions/ResourceContents"
}
},
"result_type": {
"description": "Result type discriminator. Absent values deserialize as `\"complete\"`.",
"allOf": [
{
"$ref": "#/definitions/ResultType"
}
],
"default": "complete"
}
},
"required": [
@ -2442,6 +2543,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": [
@ -2738,6 +2843,9 @@
{
"$ref": "#/definitions/CallToolResult"
},
{
"$ref": "#/definitions/InputRequiredResult"
},
{
"$ref": "#/definitions/GetTaskPayloadResult"
},

View file

@ -178,6 +178,15 @@
"null"
]
},
"resultType": {
"description": "Result type discriminator. Absent values deserialize as `\"complete\"`.",
"allOf": [
{
"$ref": "#/definitions/ResultType"
}
],
"default": "complete"
},
"structuredContent": {
"description": "An optional JSON object that represents the structured result of the tool call"
}
@ -292,6 +301,15 @@
},
"completion": {
"$ref": "#/definitions/CompletionInfo"
},
"resultType": {
"description": "Result type discriminator. Absent values deserialize as `\"complete\"`.",
"allOf": [
{
"$ref": "#/definitions/ResultType"
}
],
"default": "complete"
}
},
"required": [
@ -752,35 +770,11 @@
}
]
},
"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"
}
},
"required": [
"elicitationId"
]
},
"ElicitationSchema": {
"description": "Type-safe elicitation schema for requesting structured user input.\n\nThis enforces the MCP 2025-06-18 specification that elicitation schemas\nmust be objects with primitive-typed properties.\n\n# Example\n\n```rust\nuse rmcp::model::*;\n\nlet schema = ElicitationSchema::builder()\n .required_email(\"email\")\n .required_integer(\"age\", 0, 150)\n .optional_bool(\"newsletter\", false)\n .build();\n```",
"type": "object",
@ -948,6 +942,15 @@
"items": {
"$ref": "#/definitions/PromptMessage"
}
},
"resultType": {
"description": "Result type discriminator. Absent values deserialize as `\"complete\"`.",
"allOf": [
{
"$ref": "#/definitions/ResultType"
}
],
"default": "complete"
}
},
"required": [
@ -1205,6 +1208,77 @@
"serverInfo"
]
},
"InputRequest": {
"description": "A server-initiated request that can appear inside [`InputRequests`].\n\nPer the MCP spec, only `CreateMessageRequest` (sampling),\n`ElicitRequest` (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",
@ -1322,9 +1396,6 @@
{
"$ref": "#/definitions/Notification5"
},
{
"$ref": "#/definitions/Notification6"
},
{
"$ref": "#/definitions/CustomNotification"
}
@ -1456,6 +1527,15 @@
"items": {
"$ref": "#/definitions/Prompt"
}
},
"resultType": {
"description": "Result type discriminator. Absent values deserialize as `\"complete\"`.",
"allOf": [
{
"$ref": "#/definitions/ResultType"
}
],
"default": "complete"
}
},
"required": [
@ -1483,6 +1563,15 @@
"items": {
"$ref": "#/definitions/ResourceTemplate"
}
},
"resultType": {
"description": "Result type discriminator. Absent values deserialize as `\"complete\"`.",
"allOf": [
{
"$ref": "#/definitions/ResultType"
}
],
"default": "complete"
}
},
"required": [
@ -1510,6 +1599,15 @@
"items": {
"$ref": "#/definitions/Resource"
}
},
"resultType": {
"description": "Result type discriminator. Absent values deserialize as `\"complete\"`.",
"allOf": [
{
"$ref": "#/definitions/ResultType"
}
],
"default": "complete"
}
},
"required": [
@ -1564,6 +1662,15 @@
"null"
]
},
"resultType": {
"description": "Result type discriminator. Absent values deserialize as `\"complete\"`.",
"allOf": [
{
"$ref": "#/definitions/ResultType"
}
],
"default": "complete"
},
"tools": {
"type": "array",
"items": {
@ -1758,21 +1865,6 @@
]
},
"Notification5": {
"type": "object",
"properties": {
"method": {
"$ref": "#/definitions/ElicitationCompletionNotificationMethod"
},
"params": {
"$ref": "#/definitions/ElicitationResponseNotificationParam"
}
},
"required": [
"method",
"params"
]
},
"Notification6": {
"type": "object",
"properties": {
"method": {
@ -2129,6 +2221,15 @@
"items": {
"$ref": "#/definitions/ResourceContents"
}
},
"result_type": {
"description": "Result type discriminator. Absent values deserialize as `\"complete\"`.",
"allOf": [
{
"$ref": "#/definitions/ResultType"
}
],
"default": "complete"
}
},
"required": [
@ -2442,6 +2543,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": [
@ -2738,6 +2843,9 @@
{
"$ref": "#/definitions/CallToolResult"
},
{
"$ref": "#/definitions/InputRequiredResult"
},
{
"$ref": "#/definitions/GetTaskPayloadResult"
},

View file

@ -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"}

View file

@ -252,8 +252,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()
})
}
@ -293,9 +292,8 @@ impl ServerHandler for Counter {
_: RequestContext<RoleServer>,
) -> Result<ListResourceTemplatesResult, McpError> {
Ok(ListResourceTemplatesResult {
next_cursor: None,
resource_templates: Vec::new(),
meta: None,
..Default::default()
})
}

View file

@ -129,18 +129,9 @@ impl ElicitationServer {
)
})?;
match elicit_result {
ElicitationAction::Accept => {
// Mock notifying completion
let _ = context
.peer
.notify_url_elicitation_completed(ElicitationResponseNotificationParam::new(
"elicit_123",
))
.await;
Ok(CallToolResult::success(vec![ContentBlock::text(
ElicitationAction::Accept => Ok(CallToolResult::success(vec![ContentBlock::text(
"Elicitation via URL successful".to_string(),
)]))
}
)])),
ElicitationAction::Cancel => Ok(CallToolResult::success(vec![ContentBlock::text(
"Elicitation via URL cancelled by user".to_string(),
)])),

View file

@ -113,8 +113,7 @@ impl ServerHandler for SamplingDemoServer {
.unwrap(),
),
)],
meta: None,
next_cursor: None,
..Default::default()
})
}
}