Skip to content

Commit 7d19769

Browse files
committed
fix!: omit resultType for legacy protocol sessions
1 parent 7698802 commit 7d19769

9 files changed

Lines changed: 407 additions & 109 deletions

File tree

crates/rmcp-macros/src/prompt_handler.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ pub fn prompt_handler(attr: TokenStream, input: TokenStream) -> syn::Result<Toke
6161
) -> Result<rmcp::model::ListPromptsResult, rmcp::ErrorData> {
6262
let prompts = #router_expr.list_all();
6363
Ok(rmcp::model::ListPromptsResult {
64-
result_type: Default::default(),
64+
result_type: Some(rmcp::model::ResultType::COMPLETE),
6565
prompts,
6666
meta: #meta,
6767
next_cursor: None,

crates/rmcp-macros/src/tool_handler.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ pub fn tool_handler(attr: TokenStream, input: TokenStream) -> syn::Result<TokenS
6969
_context: rmcp::service::RequestContext<rmcp::RoleServer>,
7070
) -> Result<rmcp::model::ListToolsResult, rmcp::ErrorData> {
7171
Ok(rmcp::model::ListToolsResult{
72-
result_type: Default::default(),
72+
result_type: Some(rmcp::model::ResultType::COMPLETE),
7373
tools: #router.list_all(),
7474
meta: #result_meta,
7575
next_cursor: None,

crates/rmcp/src/handler/server.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ impl<H: ServerHandler> Service<RoleServer> for H {
5555
) -> Result<<RoleServer as ServiceRole>::Resp, McpError> {
5656
// `context` is moved into the dispatch below, so read the negotiated version first.
5757
let protocol_version = context.protocol_version();
58-
let mrtr_supported = protocol_version
58+
// SEP-2322 (`resultType` discriminator, MRTR) exists from 2026-07-28.
59+
let sep_2322_supported = protocol_version
5960
.as_ref()
6061
.is_some_and(|v| v.as_str() >= ProtocolVersion::V_2026_07_28.as_str());
6162
let requested_version = context.meta.protocol_version();
@@ -240,13 +241,18 @@ impl<H: ServerHandler> Service<RoleServer> for H {
240241
.map(ServerResult::task_ack)
241242
}
242243
};
243-
let result = result.and_then(|result| {
244-
if matches!(result, ServerResult::InputRequiredResult(_)) && !mrtr_supported {
244+
let result = result.and_then(|mut result| {
245+
if matches!(result, ServerResult::InputRequiredResult(_)) && !sep_2322_supported {
245246
Err(McpError::invalid_request(
246247
"InputRequiredResult requires negotiated protocol version 2026-07-28 or newer",
247248
None,
248249
))
249250
} else {
251+
// Peers on protocol versions older than 2026-07-28 keep the
252+
// legacy wire shape without `resultType: "complete"`.
253+
if !sep_2322_supported {
254+
result.strip_result_type_for_legacy_peer();
255+
}
250256
Ok(result)
251257
}
252258
});

crates/rmcp/src/handler/server/prompt.rs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,7 @@ impl IntoGetPromptResult for InputRequiredResult {
108108

109109
impl IntoGetPromptResult for Vec<PromptMessage> {
110110
fn into_get_prompt_result(self) -> Result<GetPromptResponse, crate::ErrorData> {
111-
Ok(GetPromptResult {
112-
result_type: Default::default(),
113-
description: None,
114-
messages: self,
115-
meta: None,
116-
}
117-
.into())
111+
Ok(GetPromptResult::new(self).into())
118112
}
119113
}
120114

crates/rmcp/src/model.rs

Lines changed: 141 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,13 @@ impl From<EmptyResult> for () {
776776
/// so unknown values are preserved rather than rejected. Servers implementing this
777777
/// protocol version MUST include `resultType` in every result. For backward
778778
/// compatibility, clients MUST treat an absent field as `"complete"`.
779+
///
780+
/// Ordinary results model the field as `Option<ResultType>`: `None` means the
781+
/// field is absent on the wire. Constructors default to `Some(COMPLETE)`, and
782+
/// the server handler strips the `"complete"` discriminator before responding
783+
/// to peers that negotiated a protocol version older than `2026-07-28`, so
784+
/// legacy sessions keep their historical wire shape (see
785+
/// [`ServerResult::strip_result_type_for_legacy_peer`]).
779786
#[derive(Debug, Clone, PartialEq, Eq)]
780787
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
781788
pub struct ResultType(Cow<'static, str>);
@@ -1473,14 +1480,22 @@ macro_rules! paginated_result {
14731480
($t:ident {
14741481
$i_item: ident: $t_item: ty
14751482
}) => {
1476-
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
1483+
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
14771484
#[serde(rename_all = "camelCase")]
14781485
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
14791486
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
14801487
pub struct $t {
1481-
/// Result type discriminator. Absent values deserialize as `"complete"`.
1482-
#[serde(default)]
1483-
pub result_type: ResultType,
1488+
/// Result type discriminator (SEP-2322). `None` means absent on the wire.
1489+
///
1490+
/// Per the [spec schema]: "Servers implementing this protocol version
1491+
/// MUST include this field. For backward compatibility, when a client
1492+
/// receives a result from a server implementing an earlier protocol
1493+
/// version (which does not include `resultType`), the client MUST treat
1494+
/// the absent field as `"complete"`."
1495+
///
1496+
/// [spec schema]: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/5bed7b30527019e34ccb0eb474636651424501f6/schema/draft/schema.ts#L225-L234
1497+
#[serde(default, skip_serializing_if = "Option::is_none")]
1498+
pub result_type: Option<ResultType>,
14841499
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
14851500
pub meta: Option<MetaObject>,
14861501
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -1502,10 +1517,16 @@ macro_rules! paginated_result {
15021517
pub $i_item: $t_item,
15031518
}
15041519

1520+
impl Default for $t {
1521+
fn default() -> Self {
1522+
Self::with_all_items(Default::default())
1523+
}
1524+
}
1525+
15051526
impl $t {
15061527
pub fn with_all_items(items: $t_item) -> Self {
15071528
Self {
1508-
result_type: ResultType::default(),
1529+
result_type: Some(ResultType::COMPLETE),
15091530
meta: None,
15101531
next_cursor: None,
15111532
ttl_ms: None,
@@ -1621,9 +1642,17 @@ pub type ReadResourceRequestParam = ReadResourceRequestParams;
16211642
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
16221643
#[non_exhaustive]
16231644
pub struct ReadResourceResult {
1624-
/// Result type discriminator. Absent values deserialize as `"complete"`.
1625-
#[serde(default)]
1626-
pub result_type: ResultType,
1645+
/// Result type discriminator (SEP-2322). `None` means absent on the wire.
1646+
///
1647+
/// Per the [spec schema]: "Servers implementing this protocol version
1648+
/// MUST include this field. For backward compatibility, when a client
1649+
/// receives a result from a server implementing an earlier protocol
1650+
/// version (which does not include `resultType`), the client MUST treat
1651+
/// the absent field as `"complete"`."
1652+
///
1653+
/// [spec schema]: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/5bed7b30527019e34ccb0eb474636651424501f6/schema/draft/schema.ts#L225-L234
1654+
#[serde(default, skip_serializing_if = "Option::is_none")]
1655+
pub result_type: Option<ResultType>,
16271656
/// Time, in milliseconds, that this result may be treated as fresh (SEP-2549).
16281657
/// Required by spec version 2026-07-28, but optional here to maintain compatibility
16291658
/// with older spec versions.
@@ -1648,7 +1677,7 @@ impl ReadResourceResult {
16481677
/// Create a new ReadResourceResult with the given contents.
16491678
pub fn new(contents: Vec<ResourceContents>) -> Self {
16501679
Self {
1651-
result_type: ResultType::default(),
1680+
result_type: Some(ResultType::COMPLETE),
16521681
ttl_ms: None,
16531682
cache_scope: None,
16541683
contents,
@@ -3208,24 +3237,38 @@ impl CompletionInfo {
32083237
}
32093238
}
32103239

3211-
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
3240+
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
32123241
#[serde(rename_all = "camelCase")]
32133242
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
32143243
#[non_exhaustive]
32153244
pub struct CompleteResult {
3216-
/// Result type discriminator. Absent values deserialize as `"complete"`.
3217-
#[serde(default)]
3218-
pub result_type: ResultType,
3245+
/// Result type discriminator (SEP-2322). `None` means absent on the wire.
3246+
///
3247+
/// Per the [spec schema]: "Servers implementing this protocol version
3248+
/// MUST include this field. For backward compatibility, when a client
3249+
/// receives a result from a server implementing an earlier protocol
3250+
/// version (which does not include `resultType`), the client MUST treat
3251+
/// the absent field as `"complete"`."
3252+
///
3253+
/// [spec schema]: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/5bed7b30527019e34ccb0eb474636651424501f6/schema/draft/schema.ts#L225-L234
3254+
#[serde(default, skip_serializing_if = "Option::is_none")]
3255+
pub result_type: Option<ResultType>,
32193256
pub completion: CompletionInfo,
32203257
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
32213258
pub meta: Option<MetaObject>,
32223259
}
32233260

3261+
impl Default for CompleteResult {
3262+
fn default() -> Self {
3263+
Self::new(CompletionInfo::default())
3264+
}
3265+
}
3266+
32243267
impl CompleteResult {
32253268
/// Create a new CompleteResult with the given completion info.
32263269
pub fn new(completion: CompletionInfo) -> Self {
32273270
Self {
3228-
result_type: ResultType::default(),
3271+
result_type: Some(ResultType::COMPLETE),
32293272
completion,
32303273
meta: None,
32313274
}
@@ -3674,14 +3717,22 @@ pub type CreateElicitationRequest = ElicitRequest;
36743717
///
36753718
/// Contains the content returned by the tool execution and an optional
36763719
/// flag indicating whether the operation resulted in an error.
3677-
#[derive(Default, Debug, Serialize, Clone, PartialEq)]
3720+
#[derive(Debug, Serialize, Clone, PartialEq)]
36783721
#[serde(rename_all = "camelCase")]
36793722
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
36803723
#[non_exhaustive]
36813724
pub struct CallToolResult {
3682-
/// Result type discriminator. Absent values deserialize as `"complete"`.
3683-
#[serde(default)]
3684-
pub result_type: ResultType,
3725+
/// Result type discriminator (SEP-2322). `None` means absent on the wire.
3726+
///
3727+
/// Per the [spec schema]: "Servers implementing this protocol version
3728+
/// MUST include this field. For backward compatibility, when a client
3729+
/// receives a result from a server implementing an earlier protocol
3730+
/// version (which does not include `resultType`), the client MUST treat
3731+
/// the absent field as `"complete"`."
3732+
///
3733+
/// [spec schema]: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/5bed7b30527019e34ccb0eb474636651424501f6/schema/draft/schema.ts#L225-L234
3734+
#[serde(default, skip_serializing_if = "Option::is_none")]
3735+
pub result_type: Option<ResultType>,
36853736
/// The content returned by the tool (text, images, etc.)
36863737
#[serde(default)]
36873738
pub content: Vec<ContentBlock>,
@@ -3710,7 +3761,7 @@ impl<'de> Deserialize<'de> for CallToolResult {
37103761
#[serde(rename_all = "camelCase")]
37113762
struct Helper {
37123763
#[serde(default)]
3713-
result_type: ResultType,
3764+
result_type: Option<ResultType>,
37143765
content: Option<Vec<ContentBlock>>,
37153766
structured_content: Option<Value>,
37163767
is_error: Option<bool>,
@@ -3741,11 +3792,23 @@ impl<'de> Deserialize<'de> for CallToolResult {
37413792
}
37423793
}
37433794

3795+
impl Default for CallToolResult {
3796+
fn default() -> Self {
3797+
CallToolResult {
3798+
result_type: Some(ResultType::COMPLETE),
3799+
content: Vec::new(),
3800+
structured_content: None,
3801+
is_error: None,
3802+
meta: None,
3803+
}
3804+
}
3805+
}
3806+
37443807
impl CallToolResult {
37453808
/// Create a successful tool result with unstructured content
37463809
pub fn success(content: Vec<ContentBlock>) -> Self {
37473810
CallToolResult {
3748-
result_type: ResultType::default(),
3811+
result_type: Some(ResultType::COMPLETE),
37493812
content,
37503813
structured_content: None,
37513814
is_error: Some(false),
@@ -3803,7 +3866,7 @@ impl CallToolResult {
38033866
/// ```
38043867
pub fn error(content: Vec<ContentBlock>) -> Self {
38053868
CallToolResult {
3806-
result_type: ResultType::default(),
3869+
result_type: Some(ResultType::COMPLETE),
38073870
content,
38083871
structured_content: None,
38093872
is_error: Some(true),
@@ -3826,7 +3889,7 @@ impl CallToolResult {
38263889
/// ```
38273890
pub fn structured(value: Value) -> Self {
38283891
CallToolResult {
3829-
result_type: ResultType::default(),
3892+
result_type: Some(ResultType::COMPLETE),
38303893
content: vec![ContentBlock::text(value.to_string())],
38313894
structured_content: Some(value),
38323895
is_error: Some(false),
@@ -3853,7 +3916,7 @@ impl CallToolResult {
38533916
/// ```
38543917
pub fn structured_error(value: Value) -> Self {
38553918
CallToolResult {
3856-
result_type: ResultType::default(),
3919+
result_type: Some(ResultType::COMPLETE),
38573920
content: vec![ContentBlock::text(value.to_string())],
38583921
structured_content: Some(value),
38593922
is_error: Some(true),
@@ -4041,26 +4104,40 @@ impl CreateMessageResult {
40414104
}
40424105
}
40434106

4044-
#[derive(Default, Debug, Serialize, Deserialize, Clone, PartialEq)]
4107+
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
40454108
#[serde(rename_all = "camelCase")]
40464109
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
40474110
#[non_exhaustive]
40484111
pub struct GetPromptResult {
4049-
/// Result type discriminator. Absent values deserialize as `"complete"`.
4050-
#[serde(default)]
4051-
pub result_type: ResultType,
4112+
/// Result type discriminator (SEP-2322). `None` means absent on the wire.
4113+
///
4114+
/// Per the [spec schema]: "Servers implementing this protocol version
4115+
/// MUST include this field. For backward compatibility, when a client
4116+
/// receives a result from a server implementing an earlier protocol
4117+
/// version (which does not include `resultType`), the client MUST treat
4118+
/// the absent field as `"complete"`."
4119+
///
4120+
/// [spec schema]: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/5bed7b30527019e34ccb0eb474636651424501f6/schema/draft/schema.ts#L225-L234
4121+
#[serde(default, skip_serializing_if = "Option::is_none")]
4122+
pub result_type: Option<ResultType>,
40524123
#[serde(skip_serializing_if = "Option::is_none")]
40534124
pub description: Option<String>,
40544125
pub messages: Vec<PromptMessage>,
40554126
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
40564127
pub meta: Option<MetaObject>,
40574128
}
40584129

4130+
impl Default for GetPromptResult {
4131+
fn default() -> Self {
4132+
Self::new(Vec::new())
4133+
}
4134+
}
4135+
40594136
impl GetPromptResult {
40604137
/// Create a new GetPromptResult with required fields.
40614138
pub fn new(messages: Vec<PromptMessage>) -> Self {
40624139
Self {
4063-
result_type: ResultType::default(),
4140+
result_type: Some(ResultType::COMPLETE),
40644141
description: None,
40654142
messages,
40664143
meta: None,
@@ -4424,6 +4501,42 @@ impl ServerResult {
44244501
pub fn task_ack(_: ()) -> ServerResult {
44254502
ServerResult::TaskAckResult(TaskAckResult::new())
44264503
}
4504+
4505+
/// Strip the SEP-2322 `resultType: "complete"` discriminator so the result
4506+
/// keeps the wire shape that predates protocol version `2026-07-28`.
4507+
///
4508+
/// The server handler calls this before responding to a peer that
4509+
/// negotiated an older protocol version, where the field did not exist and
4510+
/// strict peers may reject it. Only the `"complete"` value is stripped:
4511+
/// results whose discriminator carries meaning (`"input_required"`,
4512+
/// `"task"`) are already gated to `2026-07-28`+ sessions, and custom
4513+
/// extension values are preserved.
4514+
///
4515+
/// # Examples
4516+
///
4517+
/// ```
4518+
/// use rmcp::model::{CallToolResult, ServerResult};
4519+
///
4520+
/// let mut result = ServerResult::CallToolResult(CallToolResult::success(vec![]));
4521+
/// result.strip_result_type_for_legacy_peer();
4522+
///
4523+
/// let json = serde_json::to_value(&result).unwrap();
4524+
/// assert!(json.get("resultType").is_none());
4525+
/// ```
4526+
pub fn strip_result_type_for_legacy_peer(&mut self) {
4527+
let result_type = match self {
4528+
ServerResult::CompleteResult(r) => &mut r.result_type,
4529+
ServerResult::GetPromptResult(r) => &mut r.result_type,
4530+
ServerResult::ListPromptsResult(r) => &mut r.result_type,
4531+
ServerResult::ListResourcesResult(r) => &mut r.result_type,
4532+
ServerResult::ListResourceTemplatesResult(r) => &mut r.result_type,
4533+
ServerResult::ReadResourceResult(r) => &mut r.result_type,
4534+
ServerResult::ListToolsResult(r) => &mut r.result_type,
4535+
ServerResult::CallToolResult(r) => &mut r.result_type,
4536+
_ => return,
4537+
};
4538+
result_type.take_if(|result_type| result_type.is_complete());
4539+
}
44274540
}
44284541

44294542
pub type ServerJsonRpcMessage = JsonRpcMessage<ServerRequest, ServerResult, ServerNotification>;

0 commit comments

Comments
 (0)