feat!: align model types with MCP 2025-11-25 spec (#927)

* feat!: align model types with MCP 2025-11-25 spec

* fix: complete 2025-11-25 model spec conformance
This commit is contained in:
Dale Seo 2026-06-24 20:41:13 -04:00 • committed by GitHub
commit 77932141e3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 4321 additions and 3886 deletions

View file

@ -11,8 +11,8 @@ use anyhow::{Result, anyhow};
use rmcp::{
ServiceExt,
model::{
CallToolRequestParams, CallToolResult, ClientRequest, GetTaskInfoParams,
GetTaskResultParams, JsonObject, Request, ServerResult, TaskStatus,
CallToolRequestParams, CallToolResult, ClientRequest, GetTaskParams, GetTaskPayloadParams,
Request, ServerResult, TaskMetadata, TaskStatus,
},
object,
transport::{ConfigureCommandExt, TokioChildProcess},
@ -53,14 +53,14 @@ async fn main() -> Result<()> {
.await?;
tracing::info!("quick_echo -> {echo:#?}");
// 2) Task call. `slow_sum` is task_support = required, so we MUST attach a
// `task` object. An empty object is fine — clients can stash arbitrary
// metadata here that the server-side `OperationDescriptor` will keep.
// 2) Task call. `slow_sum` is task_support = required, so we MUST attach
// `task` metadata. An empty `TaskMetadata` is fine; use `.with_ttl(...)`
// to set a retention window.
let create = client
.send_request(ClientRequest::CallToolRequest(Request::new(
CallToolRequestParams::new("slow_sum")
.with_arguments(object!({ "a": 40, "b": 2 }))
.with_task(JsonObject::new()),
.with_task(TaskMetadata::new()),
)))
.await?;
let ServerResult::CreateTaskResult(create) = create else {
@ -77,11 +77,8 @@ async fn main() -> Result<()> {
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
let info = client
.send_request(ClientRequest::GetTaskInfoRequest(Request::new(
GetTaskInfoParams {
meta: None,
task_id: task_id.clone(),
},
.send_request(ClientRequest::GetTaskRequest(Request::new(
GetTaskParams::new(task_id.clone()),
)))
.await?;
let ServerResult::GetTaskResult(info) = info else {
@ -108,11 +105,8 @@ async fn main() -> Result<()> {
// here. (For a non-tool task the same value would surface as
// `ServerResult::CustomResult` and need manual `serde_json::from_value`.)
let payload = client
.send_request(ClientRequest::GetTaskResultRequest(Request::new(
GetTaskResultParams {
meta: None,
task_id: task_id.clone(),
},
.send_request(ClientRequest::GetTaskPayloadRequest(Request::new(
GetTaskPayloadParams::new(task_id.clone()),
)))
.await?;
let call_result: CallToolResult = match payload {

View file

@ -94,14 +94,14 @@ impl Counter {
}
fn _create_resource_text(&self, uri: &str, name: &str) -> Resource {
RawResource::new(uri, name.to_string()).no_annotation()
Resource::new(uri, name.to_string())
}
#[tool(description = "Increment the counter by 1")]
async fn increment(&self) -> Result<CallToolResult, McpError> {
let mut counter = self.counter.lock().await;
*counter += 1;
Ok(CallToolResult::success(vec![Content::text(
Ok(CallToolResult::success(vec![ContentBlock::text(
counter.to_string(),
)]))
}
@ -110,7 +110,7 @@ impl Counter {
async fn decrement(&self) -> Result<CallToolResult, McpError> {
let mut counter = self.counter.lock().await;
*counter -= 1;
Ok(CallToolResult::success(vec![Content::text(
Ok(CallToolResult::success(vec![ContentBlock::text(
counter.to_string(),
)]))
}
@ -118,7 +118,7 @@ impl Counter {
#[tool(description = "Get the current counter value")]
async fn get_value(&self) -> Result<CallToolResult, McpError> {
let counter = self.counter.lock().await;
Ok(CallToolResult::success(vec![Content::text(
Ok(CallToolResult::success(vec![ContentBlock::text(
counter.to_string(),
)]))
}
@ -129,19 +129,19 @@ impl Counter {
)]
async fn long_task(&self) -> Result<CallToolResult, McpError> {
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
Ok(CallToolResult::success(vec![Content::text(
Ok(CallToolResult::success(vec![ContentBlock::text(
"Long task completed",
)]))
}
#[tool(description = "Say hello to the client")]
fn say_hello(&self) -> Result<CallToolResult, McpError> {
Ok(CallToolResult::success(vec![Content::text("hello")]))
Ok(CallToolResult::success(vec![ContentBlock::text("hello")]))
}
#[tool(description = "Repeat what you say")]
fn echo(&self, Parameters(object): Parameters<JsonObject>) -> Result<CallToolResult, McpError> {
Ok(CallToolResult::success(vec![Content::text(
Ok(CallToolResult::success(vec![ContentBlock::text(
serde_json::Value::Object(object).to_string(),
)]))
}
@ -151,7 +151,7 @@ impl Counter {
&self,
Parameters(StructRequest { a, b }): Parameters<StructRequest>,
) -> Result<CallToolResult, McpError> {
Ok(CallToolResult::success(vec![Content::text(
Ok(CallToolResult::success(vec![ContentBlock::text(
(a + b).to_string(),
)]))
}
@ -166,8 +166,8 @@ impl Counter {
.map(|v| v.to_str().unwrap_or("(non-ascii)").to_owned());
match session_id {
Some(id) => Ok(CallToolResult::success(vec![Content::text(id)])),
None => Ok(CallToolResult::success(vec![Content::text(
Some(id) => Ok(CallToolResult::success(vec![ContentBlock::text(id)])),
None => Ok(CallToolResult::success(vec![ContentBlock::text(
"no session (not running over streamable HTTP?)",
)])),
}
@ -190,10 +190,7 @@ impl Counter {
"This is an example prompt with your message here: '{}'",
args.message
);
Ok(vec![PromptMessage::new_text(
PromptMessageRole::User,
prompt,
)])
Ok(vec![PromptMessage::new_text(Role::User, prompt)])
}
/// Analyze the current counter value and suggest next steps
@ -209,11 +206,11 @@ impl Counter {
let messages = vec![
PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
"I'll analyze the counter situation and suggest the best approach.",
),
PromptMessage::new_text(
PromptMessageRole::User,
Role::User,
format!(
"Current counter value: {}\nGoal value: {}\nDifference: {}\nStrategy preference: {}\n\nPlease analyze the situation and suggest the best approach to reach the goal.",
current_value, args.goal, difference, strategy
@ -406,12 +403,7 @@ mod tests {
});
let client_service = client.serve(client_transport).await?;
let mut task_meta = serde_json::Map::new();
task_meta.insert(
"source".into(),
serde_json::Value::String("integration-test".into()),
);
let params = CallToolRequestParams::new("long_task").with_task(task_meta);
let params = CallToolRequestParams::new("long_task").with_task(TaskMetadata::new());
let response = client_service
.send_request(ClientRequest::CallToolRequest(Request::new(params.clone())))
.await?;

View file

@ -97,12 +97,12 @@ impl ProgressDemo {
let chunk_str = String::from_utf8_lossy(&chunk);
counter += 1;
// create progress notification param
let progress_param = ProgressNotificationParam {
progress_token: ProgressToken(progress_token.clone()),
progress: counter as f64,
total: Some(5.0),
message: Some(chunk_str.to_string()),
};
let progress_param = ProgressNotificationParam::new(
ProgressToken(progress_token.clone()),
counter as f64,
)
.with_total(5.0)
.with_message(chunk_str.to_string());
match ctx.peer.notify_progress(progress_param).await {
Ok(_) => {
@ -122,7 +122,7 @@ impl ProgressDemo {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
Ok(CallToolResult::success(vec![Content::text(format!(
Ok(CallToolResult::success(vec![ContentBlock::text(format!(
"Processed {} records successfully",
counter
))]))

View file

@ -16,7 +16,7 @@ use std::sync::Arc;
use rmcp::{
ErrorData as McpError, ServerHandler,
handler::server::{router::tool::ToolRouter, wrapper::Parameters},
model::{CallToolResult, Content},
model::{CallToolResult, ContentBlock},
schemars, task_handler,
task_manager::OperationProcessor,
tool, tool_handler, tool_router,
@ -70,7 +70,7 @@ impl TaskDemo {
Parameters(SumArgs { a, b }): Parameters<SumArgs>,
) -> Result<CallToolResult, McpError> {
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
Ok(CallToolResult::success(vec![Content::text(
Ok(CallToolResult::success(vec![ContentBlock::text(
(a + b).to_string(),
)]))
}
@ -81,7 +81,7 @@ impl TaskDemo {
&self,
Parameters(EchoArgs { message }): Parameters<EchoArgs>,
) -> Result<CallToolResult, McpError> {
Ok(CallToolResult::success(vec![Content::text(message)]))
Ok(CallToolResult::success(vec![ContentBlock::text(message)]))
}
}

View file

@ -203,11 +203,11 @@ impl SqlQueryServer {
let messages = if args.operation.is_empty() {
vec![
PromptMessage::new_text(
PromptMessageRole::User,
Role::User,
"I need help building a SQL query. Where should I start?",
),
PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
"I'll help you build a SQL query step by step. First, what type of operation do you want to perform? \
Choose from: SELECT (to read data), INSERT (to add data), UPDATE (to modify data), or DELETE (to remove data).",
),
@ -215,11 +215,11 @@ impl SqlQueryServer {
} else if args.table.is_empty() {
vec![
PromptMessage::new_text(
PromptMessageRole::User,
Role::User,
format!("I want to {} data. What's next?", args.operation),
),
PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
format!(
"Great! For a {} operation, I need to know which table you want to work with. \
What's the name of your database table?",
@ -277,11 +277,11 @@ impl SqlQueryServer {
vec![
PromptMessage::new_text(
PromptMessageRole::User,
Role::User,
"Generate the SQL query based on my parameters and explain what it does.",
),
PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
format!(
"Here's your SQL query:\n\n```sql\n{}\n```\n\nThis query will {} the {} table.",
query,
@ -405,11 +405,8 @@ impl ServerHandler for SqlQueryServer {
let suggestions = self.fuzzy_match(&request.argument.value, &candidates);
let completion = CompletionInfo {
values: suggestions,
total: None,
has_more: Some(false),
};
let completion = CompletionInfo::with_pagination(suggestions, None, false)
.map_err(|e| McpError::internal_error(e, None))?;
Ok(CompleteResult::new(completion))
}

View file

@ -114,7 +114,7 @@ impl ElicitationEnumFormServer {
#[tool(description = "Get current enum selection form")]
async fn get_enum_form(&self) -> Result<CallToolResult, McpError> {
let guard = self.selection.lock().await;
Ok(CallToolResult::success(vec![Content::text(format!(
Ok(CallToolResult::success(vec![ContentBlock::text(format!(
"{}",
*guard
))]))
@ -133,13 +133,13 @@ impl ElicitationEnumFormServer {
Ok(Some(form)) => {
let mut guard = self.selection.lock().await;
*guard = form;
Ok(CallToolResult::success(vec![Content::text(format!(
Ok(CallToolResult::success(vec![ContentBlock::text(format!(
"Updated Selection:\n{}",
*guard
))]))
}
Ok(None) => {
return Ok(CallToolResult::success(vec![Content::text(
return Ok(CallToolResult::success(vec![ContentBlock::text(
"Elicitation cancelled by user.",
)]));
}

View file

@ -94,7 +94,7 @@ impl ElicitationServer {
}
};
Ok(CallToolResult::success(vec![Content::text(format!(
Ok(CallToolResult::success(vec![ContentBlock::text(format!(
"{} {}!",
request.greeting, user_name
))]))
@ -103,7 +103,7 @@ impl ElicitationServer {
#[tool(description = "Reset stored user name")]
async fn reset_name(&self) -> Result<CallToolResult, McpError> {
*self.user_name.lock().await = None;
Ok(CallToolResult::success(vec![Content::text(
Ok(CallToolResult::success(vec![ContentBlock::text(
"User name reset. Next greeting will ask for name again.".to_string(),
)]))
}
@ -133,20 +133,23 @@ impl ElicitationServer {
// Mock notifying completion
let _ = context
.peer
.notify_url_elicitation_completed(ElicitationResponseNotificationParam {
elicitation_id: "elicit_123".to_string(),
})
.notify_url_elicitation_completed(ElicitationResponseNotificationParam::new(
"elicit_123",
))
.await;
Ok(CallToolResult::success(vec![Content::text(
Ok(CallToolResult::success(vec![ContentBlock::text(
"Elicitation via URL successful".to_string(),
)]))
}
ElicitationAction::Cancel => Ok(CallToolResult::success(vec![Content::text(
ElicitationAction::Cancel => Ok(CallToolResult::success(vec![ContentBlock::text(
"Elicitation via URL cancelled by user".to_string(),
)])),
ElicitationAction::Decline => Ok(CallToolResult::error(vec![Content::text(
ElicitationAction::Decline => Ok(CallToolResult::error(vec![ContentBlock::text(
"Elicitation via URL declined by user".to_string(),
)])),
_ => Ok(CallToolResult::error(vec![ContentBlock::text(
"Unknown elicitation action".to_string(),
)])),
}
}
}

View file

@ -121,12 +121,9 @@ impl PromptServer {
)]
async fn greeting(&self) -> Vec<PromptMessage> {
vec![
PromptMessage::new_text(Role::User, "Hello! I'd like to start our conversation."),
PromptMessage::new_text(
PromptMessageRole::User,
"Hello! I'd like to start our conversation.",
),
PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
"Hello! I'm here to help. What would you like to discuss today?",
),
]
@ -148,14 +145,14 @@ impl PromptServer {
let messages = vec![
PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
format!(
"You are an expert {} code reviewer. The user's expertise level is {}.",
args.language, prefs.expertise_level
),
),
PromptMessage::new_text(
PromptMessageRole::User,
Role::User,
format!(
"Please review the {} code at '{}'. Focus on: {}",
args.language,
@ -164,7 +161,7 @@ impl PromptServer {
),
),
PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
format!(
"I'll review your {} code focusing on {}. Let me analyze the code at '{}'...",
args.language,
@ -203,14 +200,14 @@ impl PromptServer {
Ok(vec![
PromptMessage::new_text(
PromptMessageRole::User,
Role::User,
format!(
"I have {} data that needs {} analysis. Context: {}",
args.data_type, args.analysis_type, context
),
),
PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
format!(
"I'll help you analyze your {} data using {} techniques. Based on your context, \
I'll focus on providing actionable insights.",
@ -233,7 +230,7 @@ impl PromptServer {
let mut messages = vec![
PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
format!(
"You are a writing assistant helping create {} content for {}. \
Use a {} tone.",
@ -241,7 +238,7 @@ impl PromptServer {
),
),
PromptMessage::new_text(
PromptMessageRole::User,
Role::User,
format!(
"I need help writing {} for {}. Key points to cover: {}",
args.content_type,
@ -250,7 +247,7 @@ impl PromptServer {
),
),
PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
"I'll help you create that content. Let me structure it based on your key points.",
),
];
@ -258,11 +255,11 @@ impl PromptServer {
// Add a message for each key point
for (i, point) in args.key_points.iter().enumerate() {
messages.push(PromptMessage::new_text(
PromptMessageRole::User,
Role::User,
format!("For point {}: {}, what would you suggest?", i + 1, point),
));
messages.push(PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
format!("For '{}', I recommend...", point),
));
}
@ -291,14 +288,14 @@ impl PromptServer {
let mut messages = vec![
PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
format!(
"You are a debugging expert for {}. Help diagnose and fix issues.",
args.stack.join(", ")
),
),
PromptMessage::new_text(
PromptMessageRole::User,
Role::User,
format!(
"I'm encountering this error: {}\nStack: {}",
args.error_message,
@ -311,18 +308,18 @@ impl PromptServer {
if let Some(tried) = args.tried_solutions {
if !tried.is_empty() {
messages.push(PromptMessage::new_text(
PromptMessageRole::User,
Role::User,
format!("I've already tried: {}", tried.join(", ")),
));
messages.push(PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
"I see you've already attempted some solutions. Let me suggest different approaches.",
));
}
}
messages.push(PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
"Let's debug this systematically. First, let me understand the error context better.",
));
@ -343,18 +340,18 @@ impl PromptServer {
Ok(vec![
PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
format!(
"Create a learning path for someone at {} level who prefers {} language explanations.",
prefs.expertise_level, prefs.preferred_language
),
),
PromptMessage::new_text(
PromptMessageRole::User,
Role::User,
"What should I learn next to improve my programming skills?",
),
PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
format!(
"Based on your {} expertise level, I recommend the following learning path...",
prefs.expertise_level

View file

@ -69,7 +69,7 @@ impl ServerHandler for SamplingDemoServer {
)
})?;
tracing::debug!("Response: {:?}", response);
Ok(CallToolResult::success(vec![Content::text(format!(
Ok(CallToolResult::success(vec![ContentBlock::text(format!(
"Question: {}\nAnswer: {}",
question,
response