Skip to content

Commit a50814a

Browse files
committed
fix: omit resultType for legacy protocol sessions
1 parent 7698802 commit a50814a

9 files changed

Lines changed: 372 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: 106 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,15 @@ 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. `None` means absent on the wire,
1489+
/// which peers must treat as `"complete"` (SEP-2322).
1490+
#[serde(default, skip_serializing_if = "Option::is_none")]
1491+
pub result_type: Option<ResultType>,
14841492
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
14851493
pub meta: Option<MetaObject>,
14861494
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -1502,10 +1510,16 @@ macro_rules! paginated_result {
15021510
pub $i_item: $t_item,
15031511
}
15041512

1513+
impl Default for $t {
1514+
fn default() -> Self {
1515+
Self::with_all_items(Default::default())
1516+
}
1517+
}
1518+
15051519
impl $t {
15061520
pub fn with_all_items(items: $t_item) -> Self {
15071521
Self {
1508-
result_type: ResultType::default(),
1522+
result_type: Some(ResultType::COMPLETE),
15091523
meta: None,
15101524
next_cursor: None,
15111525
ttl_ms: None,
@@ -1621,9 +1635,10 @@ pub type ReadResourceRequestParam = ReadResourceRequestParams;
16211635
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
16221636
#[non_exhaustive]
16231637
pub struct ReadResourceResult {
1624-
/// Result type discriminator. Absent values deserialize as `"complete"`.
1625-
#[serde(default)]
1626-
pub result_type: ResultType,
1638+
/// Result type discriminator. `None` means absent on the wire,
1639+
/// which peers must treat as `"complete"` (SEP-2322).
1640+
#[serde(default, skip_serializing_if = "Option::is_none")]
1641+
pub result_type: Option<ResultType>,
16271642
/// Time, in milliseconds, that this result may be treated as fresh (SEP-2549).
16281643
/// Required by spec version 2026-07-28, but optional here to maintain compatibility
16291644
/// with older spec versions.
@@ -1648,7 +1663,7 @@ impl ReadResourceResult {
16481663
/// Create a new ReadResourceResult with the given contents.
16491664
pub fn new(contents: Vec<ResourceContents>) -> Self {
16501665
Self {
1651-
result_type: ResultType::default(),
1666+
result_type: Some(ResultType::COMPLETE),
16521667
ttl_ms: None,
16531668
cache_scope: None,
16541669
contents,
@@ -3208,24 +3223,31 @@ impl CompletionInfo {
32083223
}
32093224
}
32103225

3211-
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
3226+
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
32123227
#[serde(rename_all = "camelCase")]
32133228
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
32143229
#[non_exhaustive]
32153230
pub struct CompleteResult {
3216-
/// Result type discriminator. Absent values deserialize as `"complete"`.
3217-
#[serde(default)]
3218-
pub result_type: ResultType,
3231+
/// Result type discriminator. `None` means absent on the wire,
3232+
/// which peers must treat as `"complete"` (SEP-2322).
3233+
#[serde(default, skip_serializing_if = "Option::is_none")]
3234+
pub result_type: Option<ResultType>,
32193235
pub completion: CompletionInfo,
32203236
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
32213237
pub meta: Option<MetaObject>,
32223238
}
32233239

3240+
impl Default for CompleteResult {
3241+
fn default() -> Self {
3242+
Self::new(CompletionInfo::default())
3243+
}
3244+
}
3245+
32243246
impl CompleteResult {
32253247
/// Create a new CompleteResult with the given completion info.
32263248
pub fn new(completion: CompletionInfo) -> Self {
32273249
Self {
3228-
result_type: ResultType::default(),
3250+
result_type: Some(ResultType::COMPLETE),
32293251
completion,
32303252
meta: None,
32313253
}
@@ -3674,14 +3696,15 @@ pub type CreateElicitationRequest = ElicitRequest;
36743696
///
36753697
/// Contains the content returned by the tool execution and an optional
36763698
/// flag indicating whether the operation resulted in an error.
3677-
#[derive(Default, Debug, Serialize, Clone, PartialEq)]
3699+
#[derive(Debug, Serialize, Clone, PartialEq)]
36783700
#[serde(rename_all = "camelCase")]
36793701
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
36803702
#[non_exhaustive]
36813703
pub struct CallToolResult {
3682-
/// Result type discriminator. Absent values deserialize as `"complete"`.
3683-
#[serde(default)]
3684-
pub result_type: ResultType,
3704+
/// Result type discriminator. `None` means absent on the wire,
3705+
/// which peers must treat as `"complete"` (SEP-2322).
3706+
#[serde(default, skip_serializing_if = "Option::is_none")]
3707+
pub result_type: Option<ResultType>,
36853708
/// The content returned by the tool (text, images, etc.)
36863709
#[serde(default)]
36873710
pub content: Vec<ContentBlock>,
@@ -3710,7 +3733,7 @@ impl<'de> Deserialize<'de> for CallToolResult {
37103733
#[serde(rename_all = "camelCase")]
37113734
struct Helper {
37123735
#[serde(default)]
3713-
result_type: ResultType,
3736+
result_type: Option<ResultType>,
37143737
content: Option<Vec<ContentBlock>>,
37153738
structured_content: Option<Value>,
37163739
is_error: Option<bool>,
@@ -3741,11 +3764,23 @@ impl<'de> Deserialize<'de> for CallToolResult {
37413764
}
37423765
}
37433766

3767+
impl Default for CallToolResult {
3768+
fn default() -> Self {
3769+
CallToolResult {
3770+
result_type: Some(ResultType::COMPLETE),
3771+
content: Vec::new(),
3772+
structured_content: None,
3773+
is_error: None,
3774+
meta: None,
3775+
}
3776+
}
3777+
}
3778+
37443779
impl CallToolResult {
37453780
/// Create a successful tool result with unstructured content
37463781
pub fn success(content: Vec<ContentBlock>) -> Self {
37473782
CallToolResult {
3748-
result_type: ResultType::default(),
3783+
result_type: Some(ResultType::COMPLETE),
37493784
content,
37503785
structured_content: None,
37513786
is_error: Some(false),
@@ -3803,7 +3838,7 @@ impl CallToolResult {
38033838
/// ```
38043839
pub fn error(content: Vec<ContentBlock>) -> Self {
38053840
CallToolResult {
3806-
result_type: ResultType::default(),
3841+
result_type: Some(ResultType::COMPLETE),
38073842
content,
38083843
structured_content: None,
38093844
is_error: Some(true),
@@ -3826,7 +3861,7 @@ impl CallToolResult {
38263861
/// ```
38273862
pub fn structured(value: Value) -> Self {
38283863
CallToolResult {
3829-
result_type: ResultType::default(),
3864+
result_type: Some(ResultType::COMPLETE),
38303865
content: vec![ContentBlock::text(value.to_string())],
38313866
structured_content: Some(value),
38323867
is_error: Some(false),
@@ -3853,7 +3888,7 @@ impl CallToolResult {
38533888
/// ```
38543889
pub fn structured_error(value: Value) -> Self {
38553890
CallToolResult {
3856-
result_type: ResultType::default(),
3891+
result_type: Some(ResultType::COMPLETE),
38573892
content: vec![ContentBlock::text(value.to_string())],
38583893
structured_content: Some(value),
38593894
is_error: Some(true),
@@ -4041,26 +4076,33 @@ impl CreateMessageResult {
40414076
}
40424077
}
40434078

4044-
#[derive(Default, Debug, Serialize, Deserialize, Clone, PartialEq)]
4079+
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
40454080
#[serde(rename_all = "camelCase")]
40464081
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
40474082
#[non_exhaustive]
40484083
pub struct GetPromptResult {
4049-
/// Result type discriminator. Absent values deserialize as `"complete"`.
4050-
#[serde(default)]
4051-
pub result_type: ResultType,
4084+
/// Result type discriminator. `None` means absent on the wire,
4085+
/// which peers must treat as `"complete"` (SEP-2322).
4086+
#[serde(default, skip_serializing_if = "Option::is_none")]
4087+
pub result_type: Option<ResultType>,
40524088
#[serde(skip_serializing_if = "Option::is_none")]
40534089
pub description: Option<String>,
40544090
pub messages: Vec<PromptMessage>,
40554091
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
40564092
pub meta: Option<MetaObject>,
40574093
}
40584094

4095+
impl Default for GetPromptResult {
4096+
fn default() -> Self {
4097+
Self::new(Vec::new())
4098+
}
4099+
}
4100+
40594101
impl GetPromptResult {
40604102
/// Create a new GetPromptResult with required fields.
40614103
pub fn new(messages: Vec<PromptMessage>) -> Self {
40624104
Self {
4063-
result_type: ResultType::default(),
4105+
result_type: Some(ResultType::COMPLETE),
40644106
description: None,
40654107
messages,
40664108
meta: None,
@@ -4424,6 +4466,42 @@ impl ServerResult {
44244466
pub fn task_ack(_: ()) -> ServerResult {
44254467
ServerResult::TaskAckResult(TaskAckResult::new())
44264468
}
4469+
4470+
/// Strip the SEP-2322 `resultType: "complete"` discriminator so the result
4471+
/// keeps the wire shape that predates protocol version `2026-07-28`.
4472+
///
4473+
/// The server handler calls this before responding to a peer that
4474+
/// negotiated an older protocol version, where the field did not exist and
4475+
/// strict peers may reject it. Only the `"complete"` value is stripped:
4476+
/// results whose discriminator carries meaning (`"input_required"`,
4477+
/// `"task"`) are already gated to `2026-07-28`+ sessions, and custom
4478+
/// extension values are preserved.
4479+
///
4480+
/// # Examples
4481+
///
4482+
/// ```
4483+
/// use rmcp::model::{CallToolResult, ServerResult};
4484+
///
4485+
/// let mut result = ServerResult::CallToolResult(CallToolResult::success(vec![]));
4486+
/// result.strip_result_type_for_legacy_peer();
4487+
///
4488+
/// let json = serde_json::to_value(&result).unwrap();
4489+
/// assert!(json.get("resultType").is_none());
4490+
/// ```
4491+
pub fn strip_result_type_for_legacy_peer(&mut self) {
4492+
let result_type = match self {
4493+
ServerResult::CompleteResult(r) => &mut r.result_type,
4494+
ServerResult::GetPromptResult(r) => &mut r.result_type,
4495+
ServerResult::ListPromptsResult(r) => &mut r.result_type,
4496+
ServerResult::ListResourcesResult(r) => &mut r.result_type,
4497+
ServerResult::ListResourceTemplatesResult(r) => &mut r.result_type,
4498+
ServerResult::ReadResourceResult(r) => &mut r.result_type,
4499+
ServerResult::ListToolsResult(r) => &mut r.result_type,
4500+
ServerResult::CallToolResult(r) => &mut r.result_type,
4501+
_ => return,
4502+
};
4503+
result_type.take_if(|result_type| result_type.is_complete());
4504+
}
44274505
}
44284506

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

0 commit comments

Comments
 (0)