Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/rmcp-macros/src/prompt_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +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(),
result_type: Some(rmcp::model::ResultType::COMPLETE),
prompts,
meta: #meta,
next_cursor: None,
Expand Down
2 changes: 1 addition & 1 deletion crates/rmcp-macros/src/tool_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +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(),
result_type: Some(rmcp::model::ResultType::COMPLETE),
tools: #router.list_all(),
meta: #result_meta,
next_cursor: None,
Expand Down
12 changes: 9 additions & 3 deletions crates/rmcp/src/handler/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ impl<H: ServerHandler> Service<RoleServer> for H {
) -> Result<<RoleServer as ServiceRole>::Resp, McpError> {
// `context` is moved into the dispatch below, so read the negotiated version first.
let protocol_version = context.protocol_version();
let mrtr_supported = protocol_version
// SEP-2322 (`resultType` discriminator, MRTR) exists from 2026-07-28.
let sep_2322_supported = protocol_version
.as_ref()
.is_some_and(|v| v.as_str() >= ProtocolVersion::V_2026_07_28.as_str());
let requested_version = context.meta.protocol_version();
Expand Down Expand Up @@ -240,13 +241,18 @@ impl<H: ServerHandler> Service<RoleServer> for H {
.map(ServerResult::task_ack)
}
};
let result = result.and_then(|result| {
if matches!(result, ServerResult::InputRequiredResult(_)) && !mrtr_supported {
let result = result.and_then(|mut result| {
if matches!(result, ServerResult::InputRequiredResult(_)) && !sep_2322_supported {
Err(McpError::invalid_request(
"InputRequiredResult requires negotiated protocol version 2026-07-28 or newer",
None,
))
} else {
// Peers on protocol versions older than 2026-07-28 keep the
// legacy wire shape without `resultType: "complete"`.
if !sep_2322_supported {
result.strip_result_type_for_legacy_peer();
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"resultType": "complete" gets striped out here for legacy clients.

Ok(result)
}
});
Expand Down
8 changes: 1 addition & 7 deletions crates/rmcp/src/handler/server/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,7 @@ impl IntoGetPromptResult for InputRequiredResult {

impl IntoGetPromptResult for Vec<PromptMessage> {
fn into_get_prompt_result(self) -> Result<GetPromptResponse, crate::ErrorData> {
Ok(GetPromptResult {
result_type: Default::default(),
description: None,
messages: self,
meta: None,
}
.into())
Ok(GetPromptResult::new(self).into())
}
}

Expand Down
174 changes: 146 additions & 28 deletions crates/rmcp/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,13 @@ impl From<EmptyResult> for () {
/// 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"`.
///
/// Ordinary results model the field as `Option<ResultType>`: `None` means the
/// field is absent on the wire. Constructors default to `Some(COMPLETE)`, and
/// the server handler strips the `"complete"` discriminator before responding
/// to peers that negotiated a protocol version older than `2026-07-28`, so
/// legacy sessions keep their historical wire shape (see
/// [`ServerResult::strip_result_type_for_legacy_peer`]).
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ResultType(Cow<'static, str>);
Expand Down Expand Up @@ -1473,14 +1480,23 @@ macro_rules! paginated_result {
($t:ident {
$i_item: ident: $t_item: ty
}) => {
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
pub struct $t {
/// Result type discriminator. Absent values deserialize as `"complete"`.
#[serde(default)]
pub result_type: ResultType,
/// Result type discriminator (SEP-2322). Required by the [spec schema]
/// for servers implementing protocol version `2026-07-28`, but optional
/// here because this type also models results from older protocol
/// versions, which do not carry the field: `None` means absent on the
/// wire, and per the spec "the client MUST treat the absent field as
/// `"complete"`". Constructors default to `Some(ResultType::COMPLETE)`;
/// the server handler clears the field when responding to peers that
/// negotiated an older version.
///
/// [spec schema]: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/5bed7b30527019e34ccb0eb474636651424501f6/schema/draft/schema.ts#L225-L234
#[serde(default, skip_serializing_if = "Option::is_none")]
pub result_type: Option<ResultType>,
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
pub meta: Option<MetaObject>,
#[serde(default, skip_serializing_if = "Option::is_none")]
Expand All @@ -1502,10 +1518,16 @@ macro_rules! paginated_result {
pub $i_item: $t_item,
}

impl Default for $t {
fn default() -> Self {
Self::with_all_items(Default::default())
}
}

impl $t {
pub fn with_all_items(items: $t_item) -> Self {
Self {
result_type: ResultType::default(),
result_type: Some(ResultType::COMPLETE),
meta: None,
next_cursor: None,
ttl_ms: None,
Expand Down Expand Up @@ -1621,9 +1643,18 @@ 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,
/// Result type discriminator (SEP-2322). Required by the [spec schema]
/// for servers implementing protocol version `2026-07-28`, but optional
/// here because this type also models results from older protocol
/// versions, which do not carry the field: `None` means absent on the
/// wire, and per the spec "the client MUST treat the absent field as
/// `"complete"`". Constructors default to `Some(ResultType::COMPLETE)`;
/// the server handler clears the field when responding to peers that
/// negotiated an older version.
///
/// [spec schema]: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/5bed7b30527019e34ccb0eb474636651424501f6/schema/draft/schema.ts#L225-L234
#[serde(default, skip_serializing_if = "Option::is_none")]
pub result_type: Option<ResultType>,
/// Time, in milliseconds, that this result may be treated as fresh (SEP-2549).
/// Required by spec version 2026-07-28, but optional here to maintain compatibility
/// with older spec versions.
Expand All @@ -1648,7 +1679,7 @@ impl ReadResourceResult {
/// Create a new ReadResourceResult with the given contents.
pub fn new(contents: Vec<ResourceContents>) -> Self {
Self {
result_type: ResultType::default(),
result_type: Some(ResultType::COMPLETE),
ttl_ms: None,
cache_scope: None,
contents,
Expand Down Expand Up @@ -3208,24 +3239,39 @@ impl CompletionInfo {
}
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
#[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,
/// Result type discriminator (SEP-2322). Required by the [spec schema]
/// for servers implementing protocol version `2026-07-28`, but optional
/// here because this type also models results from older protocol
/// versions, which do not carry the field: `None` means absent on the
/// wire, and per the spec "the client MUST treat the absent field as
/// `"complete"`". Constructors default to `Some(ResultType::COMPLETE)`;
/// the server handler clears the field when responding to peers that
/// negotiated an older version.
///
/// [spec schema]: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/5bed7b30527019e34ccb0eb474636651424501f6/schema/draft/schema.ts#L225-L234
#[serde(default, skip_serializing_if = "Option::is_none")]
pub result_type: Option<ResultType>,
pub completion: CompletionInfo,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<MetaObject>,
}

impl Default for CompleteResult {
fn default() -> Self {
Self::new(CompletionInfo::default())
}
}

impl CompleteResult {
/// Create a new CompleteResult with the given completion info.
pub fn new(completion: CompletionInfo) -> Self {
Self {
result_type: ResultType::default(),
result_type: Some(ResultType::COMPLETE),
completion,
meta: None,
}
Expand Down Expand Up @@ -3674,14 +3720,23 @@ pub type CreateElicitationRequest = ElicitRequest;
///
/// Contains the content returned by the tool execution and an optional
/// flag indicating whether the operation resulted in an error.
#[derive(Default, Debug, Serialize, Clone, PartialEq)]
#[derive(Debug, Serialize, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
#[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,
/// Result type discriminator (SEP-2322). Required by the [spec schema]
/// for servers implementing protocol version `2026-07-28`, but optional
/// here because this type also models results from older protocol
/// versions, which do not carry the field: `None` means absent on the
/// wire, and per the spec "the client MUST treat the absent field as
/// `"complete"`". Constructors default to `Some(ResultType::COMPLETE)`;
/// the server handler clears the field when responding to peers that
/// negotiated an older version.
///
/// [spec schema]: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/5bed7b30527019e34ccb0eb474636651424501f6/schema/draft/schema.ts#L225-L234
#[serde(default, skip_serializing_if = "Option::is_none")]
pub result_type: Option<ResultType>,
/// The content returned by the tool (text, images, etc.)
#[serde(default)]
pub content: Vec<ContentBlock>,
Expand Down Expand Up @@ -3710,7 +3765,7 @@ impl<'de> Deserialize<'de> for CallToolResult {
#[serde(rename_all = "camelCase")]
struct Helper {
#[serde(default)]
result_type: ResultType,
result_type: Option<ResultType>,
content: Option<Vec<ContentBlock>>,
structured_content: Option<Value>,
is_error: Option<bool>,
Expand Down Expand Up @@ -3741,11 +3796,23 @@ impl<'de> Deserialize<'de> for CallToolResult {
}
}

impl Default for CallToolResult {
fn default() -> Self {
CallToolResult {
result_type: Some(ResultType::COMPLETE),

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With Option, a handler could manually set result_type: None and ship a 2026-07-28 response missing a required field. The MUST is now guaranteed by convention instead since constructors always produce Some(COMPLETE).

content: Vec::new(),
structured_content: None,
is_error: None,
meta: None,
}
}
}

impl CallToolResult {
/// Create a successful tool result with unstructured content
pub fn success(content: Vec<ContentBlock>) -> Self {
CallToolResult {
result_type: ResultType::default(),
result_type: Some(ResultType::COMPLETE),
content,
structured_content: None,
is_error: Some(false),
Expand Down Expand Up @@ -3803,7 +3870,7 @@ impl CallToolResult {
/// ```
pub fn error(content: Vec<ContentBlock>) -> Self {
CallToolResult {
result_type: ResultType::default(),
result_type: Some(ResultType::COMPLETE),
content,
structured_content: None,
is_error: Some(true),
Expand All @@ -3826,7 +3893,7 @@ impl CallToolResult {
/// ```
pub fn structured(value: Value) -> Self {
CallToolResult {
result_type: ResultType::default(),
result_type: Some(ResultType::COMPLETE),
content: vec![ContentBlock::text(value.to_string())],
structured_content: Some(value),
is_error: Some(false),
Expand All @@ -3853,7 +3920,7 @@ impl CallToolResult {
/// ```
pub fn structured_error(value: Value) -> Self {
CallToolResult {
result_type: ResultType::default(),
result_type: Some(ResultType::COMPLETE),
content: vec![ContentBlock::text(value.to_string())],
structured_content: Some(value),
is_error: Some(true),
Expand Down Expand Up @@ -4041,26 +4108,41 @@ impl CreateMessageResult {
}
}

#[derive(Default, Debug, Serialize, Deserialize, Clone, PartialEq)]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
#[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,
/// Result type discriminator (SEP-2322). Required by the [spec schema]
/// for servers implementing protocol version `2026-07-28`, but optional
/// here because this type also models results from older protocol
/// versions, which do not carry the field: `None` means absent on the
/// wire, and per the spec "the client MUST treat the absent field as
/// `"complete"`". Constructors default to `Some(ResultType::COMPLETE)`;
/// the server handler clears the field when responding to peers that
/// negotiated an older version.
///
/// [spec schema]: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/5bed7b30527019e34ccb0eb474636651424501f6/schema/draft/schema.ts#L225-L234
#[serde(default, skip_serializing_if = "Option::is_none")]
pub result_type: Option<ResultType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub messages: Vec<PromptMessage>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<MetaObject>,
}

impl Default for GetPromptResult {
fn default() -> Self {
Self::new(Vec::new())
}
}

impl GetPromptResult {
/// Create a new GetPromptResult with required fields.
pub fn new(messages: Vec<PromptMessage>) -> Self {
Self {
result_type: ResultType::default(),
result_type: Some(ResultType::COMPLETE),
description: None,
messages,
meta: None,
Expand Down Expand Up @@ -4424,6 +4506,42 @@ impl ServerResult {
pub fn task_ack(_: ()) -> ServerResult {
ServerResult::TaskAckResult(TaskAckResult::new())
}

/// Strip the SEP-2322 `resultType: "complete"` discriminator so the result
/// keeps the wire shape that predates protocol version `2026-07-28`.
///
/// The server handler calls this before responding to a peer that
/// negotiated an older protocol version, where the field did not exist and
/// strict peers may reject it. Only the `"complete"` value is stripped:
/// results whose discriminator carries meaning (`"input_required"`,
/// `"task"`) are already gated to `2026-07-28`+ sessions, and custom
/// extension values are preserved.
///
/// # Examples
///
/// ```
/// use rmcp::model::{CallToolResult, ServerResult};
///
/// let mut result = ServerResult::CallToolResult(CallToolResult::success(vec![]));
/// result.strip_result_type_for_legacy_peer();
///
/// let json = serde_json::to_value(&result).unwrap();
/// assert!(json.get("resultType").is_none());
/// ```
pub fn strip_result_type_for_legacy_peer(&mut self) {
let result_type = match self {
ServerResult::CompleteResult(r) => &mut r.result_type,
ServerResult::GetPromptResult(r) => &mut r.result_type,
ServerResult::ListPromptsResult(r) => &mut r.result_type,
ServerResult::ListResourcesResult(r) => &mut r.result_type,
ServerResult::ListResourceTemplatesResult(r) => &mut r.result_type,
ServerResult::ReadResourceResult(r) => &mut r.result_type,
ServerResult::ListToolsResult(r) => &mut r.result_type,
ServerResult::CallToolResult(r) => &mut r.result_type,
_ => return,
};
result_type.take_if(|result_type| result_type.is_complete());
}
}

pub type ServerJsonRpcMessage = JsonRpcMessage<ServerRequest, ServerResult, ServerNotification>;
Expand Down
Loading