fix!: omit resultType for legacy protocol sessions (#1038)
This commit is contained in:
parent
76988023b2
commit
6839cfd613
9 changed files with 438 additions and 109 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -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();
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>);
|
||||
|
|
@ -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")]
|
||||
|
|
@ -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,
|
||||
|
|
@ -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.
|
||||
|
|
@ -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,
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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>,
|
||||
|
|
@ -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>,
|
||||
|
|
@ -3741,11 +3796,23 @@ impl<'de> Deserialize<'de> for CallToolResult {
|
|||
}
|
||||
}
|
||||
|
||||
impl Default for CallToolResult {
|
||||
fn default() -> Self {
|
||||
CallToolResult {
|
||||
result_type: Some(ResultType::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),
|
||||
|
|
@ -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),
|
||||
|
|
@ -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),
|
||||
|
|
@ -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),
|
||||
|
|
@ -4041,14 +4108,23 @@ 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>,
|
||||
|
|
@ -4056,11 +4132,17 @@ pub struct GetPromptResult {
|
|||
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,
|
||||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -199,13 +199,15 @@
|
|||
]
|
||||
},
|
||||
"resultType": {
|
||||
"description": "Result type discriminator. Absent values deserialize as `\"complete\"`.",
|
||||
"allOf": [
|
||||
"description": "Result type discriminator (SEP-2322). Required by the [spec schema]\nfor servers implementing protocol version `2026-07-28`, but optional\nhere because this type also models results from older protocol\nversions, which do not carry the field: `None` means absent on the\nwire, and per the spec \"the client MUST treat the absent field as\n`\"complete\"`\". Constructors default to `Some(ResultType::COMPLETE)`;\nthe server handler clears the field when responding to peers that\nnegotiated an older version.\n\n[spec schema]: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/5bed7b30527019e34ccb0eb474636651424501f6/schema/draft/schema.ts#L225-L234",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ResultType"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": "complete"
|
||||
]
|
||||
},
|
||||
"structuredContent": {
|
||||
"description": "An optional JSON object that represents the structured result of the tool call"
|
||||
|
|
@ -326,13 +328,15 @@
|
|||
"$ref": "#/definitions/CompletionInfo"
|
||||
},
|
||||
"resultType": {
|
||||
"description": "Result type discriminator. Absent values deserialize as `\"complete\"`.",
|
||||
"allOf": [
|
||||
"description": "Result type discriminator (SEP-2322). Required by the [spec schema]\nfor servers implementing protocol version `2026-07-28`, but optional\nhere because this type also models results from older protocol\nversions, which do not carry the field: `None` means absent on the\nwire, and per the spec \"the client MUST treat the absent field as\n`\"complete\"`\". Constructors default to `Some(ResultType::COMPLETE)`;\nthe server handler clears the field when responding to peers that\nnegotiated an older version.\n\n[spec schema]: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/5bed7b30527019e34ccb0eb474636651424501f6/schema/draft/schema.ts#L225-L234",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ResultType"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": "complete"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
|
@ -1140,13 +1144,15 @@
|
|||
}
|
||||
},
|
||||
"resultType": {
|
||||
"description": "Result type discriminator. Absent values deserialize as `\"complete\"`.",
|
||||
"allOf": [
|
||||
"description": "Result type discriminator (SEP-2322). Required by the [spec schema]\nfor servers implementing protocol version `2026-07-28`, but optional\nhere because this type also models results from older protocol\nversions, which do not carry the field: `None` means absent on the\nwire, and per the spec \"the client MUST treat the absent field as\n`\"complete\"`\". Constructors default to `Some(ResultType::COMPLETE)`;\nthe server handler clears the field when responding to peers that\nnegotiated an older version.\n\n[spec schema]: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/5bed7b30527019e34ccb0eb474636651424501f6/schema/draft/schema.ts#L225-L234",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ResultType"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": "complete"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
|
@ -1783,13 +1789,15 @@
|
|||
}
|
||||
},
|
||||
"resultType": {
|
||||
"description": "Result type discriminator. Absent values deserialize as `\"complete\"`.",
|
||||
"allOf": [
|
||||
"description": "Result type discriminator (SEP-2322). Required by the [spec schema]\nfor servers implementing protocol version `2026-07-28`, but optional\nhere because this type also models results from older protocol\nversions, which do not carry the field: `None` means absent on the\nwire, and per the spec \"the client MUST treat the absent field as\n`\"complete\"`\". Constructors default to `Some(ResultType::COMPLETE)`;\nthe server handler clears the field when responding to peers that\nnegotiated an older version.\n\n[spec schema]: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/5bed7b30527019e34ccb0eb474636651424501f6/schema/draft/schema.ts#L225-L234",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ResultType"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": "complete"
|
||||
]
|
||||
},
|
||||
"ttlMs": {
|
||||
"description": "Time, in milliseconds, that this result may be treated as fresh (SEP-2549).\nRequired by spec version 2026-07-28, but optional here to maintain compatibility\nwith older spec versions.",
|
||||
|
|
@ -1842,13 +1850,15 @@
|
|||
}
|
||||
},
|
||||
"resultType": {
|
||||
"description": "Result type discriminator. Absent values deserialize as `\"complete\"`.",
|
||||
"allOf": [
|
||||
"description": "Result type discriminator (SEP-2322). Required by the [spec schema]\nfor servers implementing protocol version `2026-07-28`, but optional\nhere because this type also models results from older protocol\nversions, which do not carry the field: `None` means absent on the\nwire, and per the spec \"the client MUST treat the absent field as\n`\"complete\"`\". Constructors default to `Some(ResultType::COMPLETE)`;\nthe server handler clears the field when responding to peers that\nnegotiated an older version.\n\n[spec schema]: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/5bed7b30527019e34ccb0eb474636651424501f6/schema/draft/schema.ts#L225-L234",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ResultType"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": "complete"
|
||||
]
|
||||
},
|
||||
"ttlMs": {
|
||||
"description": "Time, in milliseconds, that this result may be treated as fresh (SEP-2549).\nRequired by spec version 2026-07-28, but optional here to maintain compatibility\nwith older spec versions.",
|
||||
|
|
@ -1901,13 +1911,15 @@
|
|||
}
|
||||
},
|
||||
"resultType": {
|
||||
"description": "Result type discriminator. Absent values deserialize as `\"complete\"`.",
|
||||
"allOf": [
|
||||
"description": "Result type discriminator (SEP-2322). Required by the [spec schema]\nfor servers implementing protocol version `2026-07-28`, but optional\nhere because this type also models results from older protocol\nversions, which do not carry the field: `None` means absent on the\nwire, and per the spec \"the client MUST treat the absent field as\n`\"complete\"`\". Constructors default to `Some(ResultType::COMPLETE)`;\nthe server handler clears the field when responding to peers that\nnegotiated an older version.\n\n[spec schema]: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/5bed7b30527019e34ccb0eb474636651424501f6/schema/draft/schema.ts#L225-L234",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ResultType"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": "complete"
|
||||
]
|
||||
},
|
||||
"ttlMs": {
|
||||
"description": "Time, in milliseconds, that this result may be treated as fresh (SEP-2549).\nRequired by spec version 2026-07-28, but optional here to maintain compatibility\nwith older spec versions.",
|
||||
|
|
@ -1959,13 +1971,15 @@
|
|||
]
|
||||
},
|
||||
"resultType": {
|
||||
"description": "Result type discriminator. Absent values deserialize as `\"complete\"`.",
|
||||
"allOf": [
|
||||
"description": "Result type discriminator (SEP-2322). Required by the [spec schema]\nfor servers implementing protocol version `2026-07-28`, but optional\nhere because this type also models results from older protocol\nversions, which do not carry the field: `None` means absent on the\nwire, and per the spec \"the client MUST treat the absent field as\n`\"complete\"`\". Constructors default to `Some(ResultType::COMPLETE)`;\nthe server handler clears the field when responding to peers that\nnegotiated an older version.\n\n[spec schema]: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/5bed7b30527019e34ccb0eb474636651424501f6/schema/draft/schema.ts#L225-L234",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ResultType"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": "complete"
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
"type": "array",
|
||||
|
|
@ -2581,13 +2595,15 @@
|
|||
}
|
||||
},
|
||||
"resultType": {
|
||||
"description": "Result type discriminator. Absent values deserialize as `\"complete\"`.",
|
||||
"allOf": [
|
||||
"description": "Result type discriminator (SEP-2322). Required by the [spec schema]\nfor servers implementing protocol version `2026-07-28`, but optional\nhere because this type also models results from older protocol\nversions, which do not carry the field: `None` means absent on the\nwire, and per the spec \"the client MUST treat the absent field as\n`\"complete\"`\". Constructors default to `Some(ResultType::COMPLETE)`;\nthe server handler clears the field when responding to peers that\nnegotiated an older version.\n\n[spec schema]: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/5bed7b30527019e34ccb0eb474636651424501f6/schema/draft/schema.ts#L225-L234",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ResultType"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": "complete"
|
||||
]
|
||||
},
|
||||
"ttlMs": {
|
||||
"description": "Time, in milliseconds, that this result may be treated as fresh (SEP-2549).\nRequired by spec version 2026-07-28, but optional here to maintain compatibility\nwith older spec versions.",
|
||||
|
|
@ -2948,7 +2964,7 @@
|
|||
}
|
||||
},
|
||||
"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\"`.",
|
||||
"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\"`.\n\nOrdinary results model the field as `Option<ResultType>`: `None` means the\nfield is absent on the wire. Constructors default to `Some(COMPLETE)`, and\nthe server handler strips the `\"complete\"` discriminator before responding\nto peers that negotiated a protocol version older than `2026-07-28`, so\nlegacy sessions keep their historical wire shape (see\n[`ServerResult::strip_result_type_for_legacy_peer`]).",
|
||||
"type": "string"
|
||||
},
|
||||
"Role": {
|
||||
|
|
|
|||
|
|
@ -199,13 +199,15 @@
|
|||
]
|
||||
},
|
||||
"resultType": {
|
||||
"description": "Result type discriminator. Absent values deserialize as `\"complete\"`.",
|
||||
"allOf": [
|
||||
"description": "Result type discriminator (SEP-2322). Required by the [spec schema]\nfor servers implementing protocol version `2026-07-28`, but optional\nhere because this type also models results from older protocol\nversions, which do not carry the field: `None` means absent on the\nwire, and per the spec \"the client MUST treat the absent field as\n`\"complete\"`\". Constructors default to `Some(ResultType::COMPLETE)`;\nthe server handler clears the field when responding to peers that\nnegotiated an older version.\n\n[spec schema]: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/5bed7b30527019e34ccb0eb474636651424501f6/schema/draft/schema.ts#L225-L234",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ResultType"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": "complete"
|
||||
]
|
||||
},
|
||||
"structuredContent": {
|
||||
"description": "An optional JSON object that represents the structured result of the tool call"
|
||||
|
|
@ -326,13 +328,15 @@
|
|||
"$ref": "#/definitions/CompletionInfo"
|
||||
},
|
||||
"resultType": {
|
||||
"description": "Result type discriminator. Absent values deserialize as `\"complete\"`.",
|
||||
"allOf": [
|
||||
"description": "Result type discriminator (SEP-2322). Required by the [spec schema]\nfor servers implementing protocol version `2026-07-28`, but optional\nhere because this type also models results from older protocol\nversions, which do not carry the field: `None` means absent on the\nwire, and per the spec \"the client MUST treat the absent field as\n`\"complete\"`\". Constructors default to `Some(ResultType::COMPLETE)`;\nthe server handler clears the field when responding to peers that\nnegotiated an older version.\n\n[spec schema]: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/5bed7b30527019e34ccb0eb474636651424501f6/schema/draft/schema.ts#L225-L234",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ResultType"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": "complete"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
|
@ -1140,13 +1144,15 @@
|
|||
}
|
||||
},
|
||||
"resultType": {
|
||||
"description": "Result type discriminator. Absent values deserialize as `\"complete\"`.",
|
||||
"allOf": [
|
||||
"description": "Result type discriminator (SEP-2322). Required by the [spec schema]\nfor servers implementing protocol version `2026-07-28`, but optional\nhere because this type also models results from older protocol\nversions, which do not carry the field: `None` means absent on the\nwire, and per the spec \"the client MUST treat the absent field as\n`\"complete\"`\". Constructors default to `Some(ResultType::COMPLETE)`;\nthe server handler clears the field when responding to peers that\nnegotiated an older version.\n\n[spec schema]: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/5bed7b30527019e34ccb0eb474636651424501f6/schema/draft/schema.ts#L225-L234",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ResultType"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": "complete"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
|
@ -1783,13 +1789,15 @@
|
|||
}
|
||||
},
|
||||
"resultType": {
|
||||
"description": "Result type discriminator. Absent values deserialize as `\"complete\"`.",
|
||||
"allOf": [
|
||||
"description": "Result type discriminator (SEP-2322). Required by the [spec schema]\nfor servers implementing protocol version `2026-07-28`, but optional\nhere because this type also models results from older protocol\nversions, which do not carry the field: `None` means absent on the\nwire, and per the spec \"the client MUST treat the absent field as\n`\"complete\"`\". Constructors default to `Some(ResultType::COMPLETE)`;\nthe server handler clears the field when responding to peers that\nnegotiated an older version.\n\n[spec schema]: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/5bed7b30527019e34ccb0eb474636651424501f6/schema/draft/schema.ts#L225-L234",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ResultType"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": "complete"
|
||||
]
|
||||
},
|
||||
"ttlMs": {
|
||||
"description": "Time, in milliseconds, that this result may be treated as fresh (SEP-2549).\nRequired by spec version 2026-07-28, but optional here to maintain compatibility\nwith older spec versions.",
|
||||
|
|
@ -1842,13 +1850,15 @@
|
|||
}
|
||||
},
|
||||
"resultType": {
|
||||
"description": "Result type discriminator. Absent values deserialize as `\"complete\"`.",
|
||||
"allOf": [
|
||||
"description": "Result type discriminator (SEP-2322). Required by the [spec schema]\nfor servers implementing protocol version `2026-07-28`, but optional\nhere because this type also models results from older protocol\nversions, which do not carry the field: `None` means absent on the\nwire, and per the spec \"the client MUST treat the absent field as\n`\"complete\"`\". Constructors default to `Some(ResultType::COMPLETE)`;\nthe server handler clears the field when responding to peers that\nnegotiated an older version.\n\n[spec schema]: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/5bed7b30527019e34ccb0eb474636651424501f6/schema/draft/schema.ts#L225-L234",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ResultType"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": "complete"
|
||||
]
|
||||
},
|
||||
"ttlMs": {
|
||||
"description": "Time, in milliseconds, that this result may be treated as fresh (SEP-2549).\nRequired by spec version 2026-07-28, but optional here to maintain compatibility\nwith older spec versions.",
|
||||
|
|
@ -1901,13 +1911,15 @@
|
|||
}
|
||||
},
|
||||
"resultType": {
|
||||
"description": "Result type discriminator. Absent values deserialize as `\"complete\"`.",
|
||||
"allOf": [
|
||||
"description": "Result type discriminator (SEP-2322). Required by the [spec schema]\nfor servers implementing protocol version `2026-07-28`, but optional\nhere because this type also models results from older protocol\nversions, which do not carry the field: `None` means absent on the\nwire, and per the spec \"the client MUST treat the absent field as\n`\"complete\"`\". Constructors default to `Some(ResultType::COMPLETE)`;\nthe server handler clears the field when responding to peers that\nnegotiated an older version.\n\n[spec schema]: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/5bed7b30527019e34ccb0eb474636651424501f6/schema/draft/schema.ts#L225-L234",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ResultType"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": "complete"
|
||||
]
|
||||
},
|
||||
"ttlMs": {
|
||||
"description": "Time, in milliseconds, that this result may be treated as fresh (SEP-2549).\nRequired by spec version 2026-07-28, but optional here to maintain compatibility\nwith older spec versions.",
|
||||
|
|
@ -1959,13 +1971,15 @@
|
|||
]
|
||||
},
|
||||
"resultType": {
|
||||
"description": "Result type discriminator. Absent values deserialize as `\"complete\"`.",
|
||||
"allOf": [
|
||||
"description": "Result type discriminator (SEP-2322). Required by the [spec schema]\nfor servers implementing protocol version `2026-07-28`, but optional\nhere because this type also models results from older protocol\nversions, which do not carry the field: `None` means absent on the\nwire, and per the spec \"the client MUST treat the absent field as\n`\"complete\"`\". Constructors default to `Some(ResultType::COMPLETE)`;\nthe server handler clears the field when responding to peers that\nnegotiated an older version.\n\n[spec schema]: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/5bed7b30527019e34ccb0eb474636651424501f6/schema/draft/schema.ts#L225-L234",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ResultType"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": "complete"
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
"type": "array",
|
||||
|
|
@ -2581,13 +2595,15 @@
|
|||
}
|
||||
},
|
||||
"resultType": {
|
||||
"description": "Result type discriminator. Absent values deserialize as `\"complete\"`.",
|
||||
"allOf": [
|
||||
"description": "Result type discriminator (SEP-2322). Required by the [spec schema]\nfor servers implementing protocol version `2026-07-28`, but optional\nhere because this type also models results from older protocol\nversions, which do not carry the field: `None` means absent on the\nwire, and per the spec \"the client MUST treat the absent field as\n`\"complete\"`\". Constructors default to `Some(ResultType::COMPLETE)`;\nthe server handler clears the field when responding to peers that\nnegotiated an older version.\n\n[spec schema]: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/5bed7b30527019e34ccb0eb474636651424501f6/schema/draft/schema.ts#L225-L234",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ResultType"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": "complete"
|
||||
]
|
||||
},
|
||||
"ttlMs": {
|
||||
"description": "Time, in milliseconds, that this result may be treated as fresh (SEP-2549).\nRequired by spec version 2026-07-28, but optional here to maintain compatibility\nwith older spec versions.",
|
||||
|
|
@ -2948,7 +2964,7 @@
|
|||
}
|
||||
},
|
||||
"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\"`.",
|
||||
"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\"`.\n\nOrdinary results model the field as `Option<ResultType>`: `None` means the\nfield is absent on the wire. Constructors default to `Some(COMPLETE)`, and\nthe server handler strips the `\"complete\"` discriminator before responding\nto peers that negotiated a protocol version older than `2026-07-28`, so\nlegacy sessions keep their historical wire shape (see\n[`ServerResult::strip_result_type_for_legacy_peer`]).",
|
||||
"type": "string"
|
||||
},
|
||||
"Role": {
|
||||
|
|
|
|||
82
crates/rmcp/tests/test_result_type_version.rs
Normal file
82
crates/rmcp/tests/test_result_type_version.rs
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
//! SEP-2322: the `resultType` discriminator follows the negotiated protocol version.
|
||||
//!
|
||||
//! Peers negotiating `2026-07-28` or newer receive `resultType: "complete"` on
|
||||
//! ordinary results; older peers keep the legacy wire shape without the field.
|
||||
#![cfg(not(feature = "local"))]
|
||||
#![cfg(feature = "client")]
|
||||
|
||||
use rmcp::{
|
||||
ClientHandler, RoleServer, ServerHandler, ServiceExt,
|
||||
model::{
|
||||
CallToolRequestParams, CallToolResponse, CallToolResult, ClientInfo, ContentBlock,
|
||||
ErrorData, ProtocolVersion, ResultType,
|
||||
},
|
||||
service::RequestContext,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct ToolServer;
|
||||
|
||||
impl ServerHandler for ToolServer {
|
||||
async fn call_tool(
|
||||
&self,
|
||||
_request: CallToolRequestParams,
|
||||
_context: RequestContext<RoleServer>,
|
||||
) -> Result<CallToolResponse, ErrorData> {
|
||||
Ok(CallToolResult::success(vec![ContentBlock::text("ok")]).into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct VersionedClient {
|
||||
protocol_version: ProtocolVersion,
|
||||
}
|
||||
|
||||
impl ClientHandler for VersionedClient {
|
||||
fn get_info(&self) -> ClientInfo {
|
||||
let mut info = ClientInfo::default();
|
||||
info.protocol_version = self.protocol_version.clone();
|
||||
info
|
||||
}
|
||||
}
|
||||
|
||||
async fn call_tool_result_type(client_version: ProtocolVersion) -> Option<ResultType> {
|
||||
let (server_transport, client_transport) = tokio::io::duplex(4096);
|
||||
|
||||
let server_handle = tokio::spawn(async move {
|
||||
ToolServer.serve(server_transport).await?.waiting().await?;
|
||||
anyhow::Ok(())
|
||||
});
|
||||
|
||||
let client = VersionedClient {
|
||||
protocol_version: client_version,
|
||||
}
|
||||
.serve(client_transport)
|
||||
.await
|
||||
.expect("client should connect");
|
||||
|
||||
let result = client
|
||||
.call_tool(CallToolRequestParams::new("echo"))
|
||||
.await
|
||||
.expect("tool call should succeed");
|
||||
|
||||
client.cancel().await.expect("client should cancel");
|
||||
server_handle.await.expect("server task").expect("server");
|
||||
result.result_type
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn legacy_version_omits_result_type() {
|
||||
assert_eq!(
|
||||
call_tool_result_type(ProtocolVersion::V_2025_11_25).await,
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sep_2322_version_gets_complete_result_type() {
|
||||
assert_eq!(
|
||||
call_tool_result_type(ProtocolVersion::V_2026_07_28).await,
|
||||
Some(ResultType::COMPLETE),
|
||||
);
|
||||
}
|
||||
|
|
@ -2,10 +2,16 @@
|
|||
//!
|
||||
//! These pin the behavior that keeps older/strict peers working:
|
||||
//! - `EmptyResult` stays a bare `{}` (some peers strict-validate empty results
|
||||
//! and reject extra keys), and
|
||||
//! - ordinary results carry `resultType: "complete"`.
|
||||
//! and reject extra keys),
|
||||
//! - ordinary results carry `resultType: "complete"` by default, and
|
||||
//! - [`ServerResult::strip_result_type_for_legacy_peer`] removes the
|
||||
//! `"complete"` discriminator so legacy sessions keep their historical wire
|
||||
//! shape (round-tripping a legacy result never adds the field).
|
||||
|
||||
use rmcp::model::{CallToolResult, ContentBlock, EmptyResult, ListToolsResult};
|
||||
use rmcp::model::{
|
||||
CallToolResult, CompleteResult, ContentBlock, EmptyResult, GetPromptResult, ListToolsResult,
|
||||
ReadResourceResult, ResultType, ServerResult,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
|
|
@ -27,3 +33,94 @@ fn paginated_result_serializes_complete_result_type() {
|
|||
serde_json::to_value(ListToolsResult::default()).expect("serialize ListToolsResult");
|
||||
assert_eq!(value["resultType"], "complete");
|
||||
}
|
||||
|
||||
// Guards the convention the server handler relies on: every constructor and
|
||||
// `Default` impl produces `Some(COMPLETE)`, so 2026-07-28 sessions always
|
||||
// include the spec-required field unless a handler clears it explicitly.
|
||||
#[test]
|
||||
fn constructors_and_defaults_produce_complete_result_type() {
|
||||
let complete = Some(ResultType::COMPLETE);
|
||||
assert_eq!(CallToolResult::default().result_type, complete);
|
||||
assert_eq!(CallToolResult::success(vec![]).result_type, complete);
|
||||
assert_eq!(CallToolResult::error(vec![]).result_type, complete);
|
||||
assert_eq!(CallToolResult::structured(json!({})).result_type, complete);
|
||||
assert_eq!(
|
||||
CallToolResult::structured_error(json!({})).result_type,
|
||||
complete
|
||||
);
|
||||
assert_eq!(ListToolsResult::default().result_type, complete);
|
||||
assert_eq!(
|
||||
ListToolsResult::with_all_items(vec![]).result_type,
|
||||
complete
|
||||
);
|
||||
assert_eq!(ReadResourceResult::new(vec![]).result_type, complete);
|
||||
assert_eq!(GetPromptResult::default().result_type, complete);
|
||||
assert_eq!(GetPromptResult::new(vec![]).result_type, complete);
|
||||
assert_eq!(CompleteResult::default().result_type, complete);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absent_result_type_deserializes_as_none() {
|
||||
let legacy = json!({
|
||||
"content": [],
|
||||
"isError": false,
|
||||
});
|
||||
|
||||
let result: CallToolResult =
|
||||
serde_json::from_value(legacy).expect("deserialize legacy CallToolResult");
|
||||
|
||||
assert_eq!(result.result_type, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_call_tool_result_round_trips_without_result_type() {
|
||||
let legacy = json!({
|
||||
"content": [],
|
||||
"isError": false,
|
||||
});
|
||||
|
||||
let result: CallToolResult =
|
||||
serde_json::from_value(legacy.clone()).expect("deserialize legacy CallToolResult");
|
||||
let reserialized = serde_json::to_value(result).expect("serialize CallToolResult");
|
||||
|
||||
assert_eq!(reserialized, legacy);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strip_removes_complete_result_type() {
|
||||
let mut result =
|
||||
ServerResult::CallToolResult(CallToolResult::success(vec![ContentBlock::text("ok")]));
|
||||
result.strip_result_type_for_legacy_peer();
|
||||
|
||||
let value = serde_json::to_value(result).expect("serialize ServerResult");
|
||||
assert_eq!(
|
||||
value,
|
||||
json!({
|
||||
"content": [{ "type": "text", "text": "ok" }],
|
||||
"isError": false,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strip_removes_complete_result_type_from_paginated_result() {
|
||||
let mut result = ServerResult::ListToolsResult(ListToolsResult::default());
|
||||
result.strip_result_type_for_legacy_peer();
|
||||
|
||||
let value = serde_json::to_value(result).expect("serialize ServerResult");
|
||||
assert_eq!(value, json!({ "tools": [] }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strip_preserves_custom_result_type() {
|
||||
let streaming: ResultType =
|
||||
serde_json::from_value(json!("streaming")).expect("deserialize ResultType");
|
||||
let mut tool_result = CallToolResult::success(vec![ContentBlock::text("ok")]);
|
||||
tool_result.result_type = Some(streaming);
|
||||
|
||||
let mut result = ServerResult::CallToolResult(tool_result);
|
||||
result.strip_result_type_for_legacy_peer();
|
||||
|
||||
let value = serde_json::to_value(result).expect("serialize ServerResult");
|
||||
assert_eq!(value["resultType"], "streaming");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue