Skip to content

Commit ae7afa5

Browse files
committed
fix: complete 2025-11-25 model spec conformance
1 parent b36ce69 commit ae7afa5

14 files changed

Lines changed: 454 additions & 187 deletions

conformance/src/bin/client.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,9 @@ impl ClientHandler for ElicitationDefaultsClientHandler {
121121
_ => Some(json!({})),
122122
};
123123
let mut result = ElicitResult::new(ElicitationAction::Accept);
124-
result.content = content;
124+
if let Some(c) = content {
125+
result = result.with_content(c);
126+
}
125127
Ok(result)
126128
}
127129
}

crates/rmcp/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
12+
- [**breaking**] align `TaskStatusNotificationParam` with `NotificationParams & Task`; construct it with `TaskStatusNotificationParam::new(task)` or `task.into()`, use `.task` when exact `Task` identity matters, and rely on `Deref` for existing task field access ([#927](https://github.com/modelcontextprotocol/rust-sdk/pull/927))
13+
- [**breaking**] require `ToolResultContent.content` in sampling tool results; send `content: []` for empty results. `CallToolResult` missing-content compatibility is unchanged ([#927](https://github.com/modelcontextprotocol/rust-sdk/pull/927))
14+
1015
## [1.8.0](https://github.com/modelcontextprotocol/rust-sdk/compare/rmcp-v1.7.0...rmcp-v1.8.0) - 2026-06-22
1116

1217
### Added

crates/rmcp/src/handler/client.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ pub trait ClientHandler: Sized + Send + Sync + 'static {
128128
///
129129
/// # Example
130130
/// ```rust,ignore
131-
/// use rmcp::model::CreateElicitationRequestParam;
131+
/// use rmcp::model::ElicitRequestParams;
132132
/// use rmcp::{
133133
/// model::ErrorData as McpError,
134134
/// model::*,
@@ -139,11 +139,11 @@ pub trait ClientHandler: Sized + Send + Sync + 'static {
139139
/// impl ClientHandler for MyClient {
140140
/// async fn create_elicitation(
141141
/// &self,
142-
/// request: CreateElicitationRequestParam,
142+
/// request: ElicitRequestParams,
143143
/// context: RequestContext<RoleClient>,
144144
/// ) -> Result<ElicitResult, McpError> {
145145
/// match request {
146-
/// CreateElicitationRequestParam::FormElicitationParam {meta, message, requested_schema,} => {
146+
/// ElicitRequestParams::FormElicitationParam {meta, message, requested_schema,} => {
147147
/// // Display message to user and collect input according to requested_schema
148148
/// let user_input = get_user_input(message, requested_schema).await?;
149149
/// Ok(ElicitResult {
@@ -152,7 +152,7 @@ pub trait ClientHandler: Sized + Send + Sync + 'static {
152152
/// meta: None,
153153
/// })
154154
/// }
155-
/// CreateElicitationRequestParam::UrlElicitationParam {meta, message, url, elicitation_id,} => {
155+
/// ElicitRequestParams::UrlElicitationParam {meta, message, url, elicitation_id,} => {
156156
/// // Open URL in browser for user to complete elicitation
157157
/// open_url_in_browser(url).await?;
158158
/// Ok(ElicitResult {

crates/rmcp/src/model.rs

Lines changed: 80 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
use std::{borrow::Cow, sync::Arc};
1+
use std::{
2+
borrow::Cow,
3+
ops::{Deref, DerefMut},
4+
sync::Arc,
5+
};
26
mod annotated;
37
mod capabilities;
48
mod content;
@@ -702,6 +706,7 @@ impl CustomResult {
702706
pub struct CancelledNotificationParam {
703707
#[serde(skip_serializing_if = "Option::is_none")]
704708
pub request_id: Option<RequestId>,
709+
#[serde(skip_serializing_if = "Option::is_none")]
705710
pub reason: Option<String>,
706711
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
707712
pub meta: Option<Meta>,
@@ -2417,7 +2422,6 @@ impl Reference {
24172422
Self::Prompt(PromptReference {
24182423
name: name.into(),
24192424
title: None,
2420-
meta: None,
24212425
})
24222426
}
24232427

@@ -2474,8 +2478,6 @@ pub struct PromptReference {
24742478
pub name: String,
24752479
#[serde(skip_serializing_if = "Option::is_none")]
24762480
pub title: Option<String>,
2477-
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
2478-
pub meta: Option<Meta>,
24792481
}
24802482

24812483
impl PromptReference {
@@ -2484,7 +2486,6 @@ impl PromptReference {
24842486
Self {
24852487
name: name.into(),
24862488
title: None,
2487-
meta: None,
24882489
}
24892490
}
24902491

@@ -2561,12 +2562,20 @@ pub type ListRootsRequest = RequestNoParam<ListRootsRequestMethod>;
25612562
#[non_exhaustive]
25622563
pub struct ListRootsResult {
25632564
pub roots: Vec<Root>,
2565+
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
2566+
pub meta: Option<Meta>,
25642567
}
25652568

25662569
impl ListRootsResult {
25672570
/// Creates a new `ListRootsResult` with the given roots.
25682571
pub fn new(roots: Vec<Root>) -> Self {
2569-
Self { roots }
2572+
Self { roots, meta: None }
2573+
}
2574+
2575+
/// Sets the protocol-level metadata for this result.
2576+
pub fn with_meta(mut self, meta: Meta) -> Self {
2577+
self.meta = Some(meta);
2578+
self
25702579
}
25712580
}
25722581

@@ -3370,7 +3379,47 @@ const_string!(TaskStatusNotificationMethod = "notifications/tasks/status");
33703379
/// Parameters for a task status notification (spec `TaskStatusNotificationParams`).
33713380
///
33723381
/// The task fields are flattened at the top level: `NotificationParams & Task`.
3373-
pub type TaskStatusNotificationParam = crate::model::Task;
3382+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
3383+
#[serde(rename_all = "camelCase")]
3384+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3385+
#[non_exhaustive]
3386+
pub struct TaskStatusNotificationParam {
3387+
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
3388+
pub meta: Option<Meta>,
3389+
#[serde(flatten)]
3390+
pub task: crate::model::Task,
3391+
}
3392+
3393+
impl TaskStatusNotificationParam {
3394+
pub fn new(task: crate::model::Task) -> Self {
3395+
Self { meta: None, task }
3396+
}
3397+
3398+
pub fn with_meta(mut self, meta: Meta) -> Self {
3399+
self.meta = Some(meta);
3400+
self
3401+
}
3402+
}
3403+
3404+
impl From<crate::model::Task> for TaskStatusNotificationParam {
3405+
fn from(task: crate::model::Task) -> Self {
3406+
Self::new(task)
3407+
}
3408+
}
3409+
3410+
impl Deref for TaskStatusNotificationParam {
3411+
type Target = crate::model::Task;
3412+
3413+
fn deref(&self) -> &Self::Target {
3414+
&self.task
3415+
}
3416+
}
3417+
3418+
impl DerefMut for TaskStatusNotificationParam {
3419+
fn deref_mut(&mut self) -> &mut Self::Target {
3420+
&mut self.task
3421+
}
3422+
}
33743423

33753424
pub type TaskStatusNotification =
33763425
Notification<TaskStatusNotificationMethod, TaskStatusNotificationParam>;
@@ -3613,6 +3662,30 @@ mod tests {
36133662

36143663
use super::*;
36153664

3665+
#[test]
3666+
#[allow(deprecated)]
3667+
fn deprecated_aliases_still_resolve() {
3668+
// 하위호환: 구 이름이 새 타입으로 여전히 resolve되는지 확인.
3669+
let _: CreateElicitationResult = ElicitResult::new(ElicitationAction::Accept);
3670+
let _: GetTaskResultParams = GetTaskPayloadParams::new("task-1");
3671+
let _: ResourceReference = ResourceTemplateReference::new("res://x");
3672+
}
3673+
3674+
#[test]
3675+
fn cancelled_notification_request_id_is_optional_on_wire() {
3676+
// None → requestId 생략
3677+
let p = CancelledNotificationParam::new(None, Some("user cancelled".into()));
3678+
let v = serde_json::to_value(&p).unwrap();
3679+
assert!(v.get("requestId").is_none());
3680+
3681+
// Some → requestId 방출 + 라운드트립
3682+
let p = CancelledNotificationParam::new(Some(RequestId::Number(1)), None);
3683+
let v = serde_json::to_value(&p).unwrap();
3684+
assert_eq!(v["requestId"], json!(1));
3685+
let back: CancelledNotificationParam = serde_json::from_value(v).unwrap();
3686+
assert_eq!(back.request_id, Some(RequestId::Number(1)));
3687+
}
3688+
36163689
#[test]
36173690
fn test_notification_serde() {
36183691
let raw = json!( {

crates/rmcp/src/model/content.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,6 @@ pub struct ToolResultContent {
195195
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
196196
pub meta: Option<Meta>,
197197
pub tool_use_id: String,
198-
#[serde(default, skip_serializing_if = "Vec::is_empty")]
199198
pub content: Vec<ContentBlock>,
200199
#[serde(skip_serializing_if = "Option::is_none")]
201200
pub structured_content: Option<super::JsonObject>,

crates/rmcp/src/model/task.rs

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,20 @@ impl Task {
138138
#[non_exhaustive]
139139
pub struct CreateTaskResult {
140140
pub task: Task,
141+
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
142+
pub meta: Option<Meta>,
141143
}
142144

143145
impl CreateTaskResult {
144146
/// Create a new CreateTaskResult.
145147
pub fn new(task: Task) -> Self {
146-
Self { task }
148+
Self { task, meta: None }
149+
}
150+
151+
/// Sets the protocol-level metadata for this result.
152+
pub fn with_meta(mut self, meta: Meta) -> Self {
153+
self.meta = Some(meta);
154+
self
147155
}
148156
}
149157

@@ -224,27 +232,3 @@ impl CancelTaskResult {
224232
Self { meta: None, task }
225233
}
226234
}
227-
228-
/// Paginated list of tasks
229-
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
230-
#[serde(rename_all = "camelCase")]
231-
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
232-
#[non_exhaustive]
233-
pub struct TaskList {
234-
pub tasks: Vec<Task>,
235-
#[serde(skip_serializing_if = "Option::is_none")]
236-
pub next_cursor: Option<String>,
237-
#[serde(skip_serializing_if = "Option::is_none")]
238-
pub total: Option<u64>,
239-
}
240-
241-
impl TaskList {
242-
/// Create a new TaskList.
243-
pub fn new(tasks: Vec<Task>) -> Self {
244-
Self {
245-
tasks,
246-
next_cursor: None,
247-
total: None,
248-
}
249-
}
250-
}

crates/rmcp/tests/test_completion.rs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -156,14 +156,18 @@ fn test_completion_serialization_format() {
156156

157157
#[test]
158158
fn test_resource_reference() {
159-
// Test that ResourceTemplateReference works correctly
160-
let resource_ref = ResourceTemplateReference::new("test://uri");
161-
162-
// Test that ResourceTemplateReference works correctly
163-
let another_ref = ResourceTemplateReference::new("test://uri");
164-
165-
// They should be equivalent
166-
assert_eq!(resource_ref.uri, another_ref.uri);
159+
// ResourceTemplateReference가 `ref/resource` 와이어 태그로 직렬화/역직렬화되는지 확인
160+
let reference = Reference::for_resource("test://uri");
161+
162+
let json = serde_json::to_value(&reference).unwrap();
163+
assert_eq!(json["type"], "ref/resource");
164+
assert_eq!(json["uri"], "test://uri");
165+
166+
let back: Reference = serde_json::from_value(json).unwrap();
167+
match back {
168+
Reference::Resource(r) => assert_eq!(r.uri, "test://uri"),
169+
other => panic!("expected Reference::Resource, got {other:?}"),
170+
}
167171
}
168172

169173
#[test]

0 commit comments

Comments
 (0)