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
2 changed files with 4321 additions and 3886 deletions

View file

@ -45,48 +45,46 @@ struct ElicitationDefaultsClientHandler;
impl ClientHandler for ElicitationDefaultsClientHandler {
fn get_info(&self) -> ClientInfo {
let mut info = ClientInfo::default();
info.capabilities.elicitation = Some(ElicitationCapability {
form: Some(FormElicitationCapability {
schema_validation: Some(true),
}),
url: None,
});
info.capabilities.elicitation = Some(
ElicitationCapability::new()
.with_form(FormElicitationCapability::new().with_schema_validation(true)),
);
info
}
async fn create_elicitation(
&self,
request: CreateElicitationRequestParams,
request: ElicitRequestParams,
_cx: RequestContext<RoleClient>,
) -> Result<CreateElicitationResult, ErrorData> {
) -> Result<ElicitResult, ErrorData> {
let content = match &request {
CreateElicitationRequestParams::FormElicitationParams {
ElicitRequestParams::FormElicitationParams {
requested_schema, ..
} => {
let mut defaults = serde_json::Map::new();
for (name, prop) in &requested_schema.properties {
match prop {
PrimitiveSchema::String(s) => {
PrimitiveSchemaDefinition::String(s) => {
if let Some(d) = &s.default {
defaults.insert(name.clone(), Value::String(d.clone()));
}
}
PrimitiveSchema::Number(n) => {
PrimitiveSchemaDefinition::Number(n) => {
if let Some(d) = n.default {
defaults.insert(name.clone(), json!(d));
}
}
PrimitiveSchema::Integer(i) => {
PrimitiveSchemaDefinition::Integer(i) => {
if let Some(d) = i.default {
defaults.insert(name.clone(), json!(d));
}
}
PrimitiveSchema::Boolean(b) => {
PrimitiveSchemaDefinition::Boolean(b) => {
if let Some(d) = b.default {
defaults.insert(name.clone(), Value::Bool(d));
}
}
PrimitiveSchema::Enum(e) => {
PrimitiveSchemaDefinition::Enum(e) => {
let val = match e {
EnumSchema::Single(SingleSelectEnumSchema::Untitled(u)) => {
u.default.as_ref().map(|d| Value::String(d.clone()))
@ -109,22 +107,24 @@ impl ClientHandler for ElicitationDefaultsClientHandler {
})
}
EnumSchema::Legacy(_) => None,
_ => None,
};
if let Some(v) = val {
defaults.insert(name.clone(), v);
}
}
_ => {}
}
}
Some(Value::Object(defaults))
}
_ => Some(json!({})),
};
Ok(CreateElicitationResult {
action: ElicitationAction::Accept,
content,
meta: None,
})
let mut result = ElicitResult::new(ElicitationAction::Accept);
if let Some(c) = content {
result = result.with_content(c);
}
Ok(result)
}
}
@ -134,12 +134,10 @@ struct FullClientHandler;
impl ClientHandler for FullClientHandler {
fn get_info(&self) -> ClientInfo {
let mut info = ClientInfo::default();
info.capabilities.elicitation = Some(ElicitationCapability {
form: Some(FormElicitationCapability {
schema_validation: Some(true),
}),
url: None,
});
info.capabilities.elicitation = Some(
ElicitationCapability::new()
.with_form(FormElicitationCapability::new().with_schema_validation(true)),
);
info
}
@ -158,7 +156,7 @@ impl ClientHandler for FullClientHandler {
Ok(CreateMessageResult::new(
SamplingMessage::new(
Role::Assistant,
SamplingMessageContent::text(format!(
SamplingMessageContentBlock::text(format!(
"This is a mock LLM response to: {}",
prompt_text
)),
@ -170,14 +168,11 @@ impl ClientHandler for FullClientHandler {
async fn create_elicitation(
&self,
_request: CreateElicitationRequestParams,
_request: ElicitRequestParams,
_cx: RequestContext<RoleClient>,
) -> Result<CreateElicitationResult, ErrorData> {
Ok(CreateElicitationResult {
action: ElicitationAction::Accept,
content: Some(json!({"username": "testuser", "email": "test@example.com"})),
meta: None,
})
) -> Result<ElicitResult, ErrorData> {
Ok(ElicitResult::new(ElicitationAction::Accept)
.with_content(json!({"username": "testuser", "email": "test@example.com"})))
}
}

View file

@ -217,25 +217,21 @@ impl ServerHandler for ConformanceServer {
) -> Result<CallToolResult, ErrorData> {
let args = request.arguments.unwrap_or_default();
match request.name.as_ref() {
"test_simple_text" => Ok(CallToolResult::success(vec![Content::text(
"test_simple_text" => Ok(CallToolResult::success(vec![ContentBlock::text(
"This is a simple text response for testing.",
)])),
"test_image_content" => Ok(CallToolResult::success(vec![Content::image(
"test_image_content" => Ok(CallToolResult::success(vec![ContentBlock::image(
TEST_IMAGE_DATA,
"image/png",
)])),
"test_audio_content" => {
let audio = RawContent::Audio(RawAudioContent {
data: TEST_AUDIO_DATA.into(),
mime_type: "audio/wav".into(),
})
.no_annotation();
let audio = ContentBlock::Audio(AudioContent::new(TEST_AUDIO_DATA, "audio/wav"));
Ok(CallToolResult::success(vec![audio]))
}
"test_embedded_resource" => Ok(CallToolResult::success(vec![Content::resource(
"test_embedded_resource" => Ok(CallToolResult::success(vec![ContentBlock::resource(
ResourceContents::TextResourceContents {
uri: "test://embedded-resource".into(),
mime_type: Some("text/plain".into()),
@ -245,9 +241,9 @@ impl ServerHandler for ConformanceServer {
)])),
"test_multiple_content_types" => Ok(CallToolResult::success(vec![
Content::text("Multiple content types test:"),
Content::image(TEST_IMAGE_DATA, "image/png"),
Content::resource(ResourceContents::TextResourceContents {
ContentBlock::text("Multiple content types test:"),
ContentBlock::image(TEST_IMAGE_DATA, "image/png"),
ContentBlock::resource(ResourceContents::TextResourceContents {
uri: "test://mixed-content-resource".into(),
mime_type: Some("application/json".into()),
text: r#"{"test":"data","value":123}"#.into(),
@ -263,21 +259,20 @@ impl ServerHandler for ConformanceServer {
] {
let _ = cx
.peer
.notify_logging_message(LoggingMessageNotificationParam {
level: LoggingLevel::Info,
logger: Some("conformance-server".into()),
data: json!(msg),
})
.notify_logging_message(
LoggingMessageNotificationParam::new(LoggingLevel::Info, json!(msg))
.with_logger("conformance-server"),
)
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
}
Ok(CallToolResult::success(vec![Content::text(
Ok(CallToolResult::success(vec![ContentBlock::text(
"Logging test completed",
)]))
}
"test_error_handling" => Ok(CallToolResult::error(vec![Content::text(
"test_error_handling" => Ok(CallToolResult::error(vec![ContentBlock::text(
"This tool intentionally returns an error for testing",
)])),
@ -290,18 +285,17 @@ impl ServerHandler for ConformanceServer {
if let Some(token) = &progress_token {
let _ = cx
.peer
.notify_progress(ProgressNotificationParam {
progress_token: token.clone(),
progress,
total: Some(100.0),
message: Some(message.into()),
})
.notify_progress(
ProgressNotificationParam::new(token.clone(), progress)
.with_total(100.0)
.with_message(message),
)
.await;
}
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
}
Ok(CallToolResult::success(vec![Content::text(
Ok(CallToolResult::success(vec![ContentBlock::text(
"Progress test completed",
)]))
}
@ -328,12 +322,12 @@ impl ServerHandler for ConformanceServer {
.and_then(|c| c.as_text())
.map(|t| t.text.clone())
.unwrap_or_else(|| "No text response".into());
Ok(CallToolResult::success(vec![Content::text(format!(
Ok(CallToolResult::success(vec![ContentBlock::text(format!(
"LLM response: {}",
text
))]))
}
Err(e) => Ok(CallToolResult::error(vec![Content::text(format!(
Err(e) => Ok(CallToolResult::error(vec![ContentBlock::text(format!(
"Sampling error: {}",
e
))])),
@ -365,23 +359,24 @@ impl ServerHandler for ConformanceServer {
match cx
.peer
.create_elicitation(CreateElicitationRequestParams::FormElicitationParams {
.create_elicitation(ElicitRequestParams::FormElicitationParams {
meta: None,
message: message.into(),
requested_schema: schema,
})
.await
{
Ok(result) => Ok(CallToolResult::success(vec![Content::text(format!(
Ok(result) => Ok(CallToolResult::success(vec![ContentBlock::text(format!(
"User response: action={}, content={:?}",
match result.action {
ElicitationAction::Accept => "accept",
ElicitationAction::Decline => "decline",
ElicitationAction::Cancel => "cancel",
_ => "unknown",
},
result.content
))])),
Err(e) => Ok(CallToolResult::error(vec![Content::text(format!(
Err(e) => Ok(CallToolResult::error(vec![ContentBlock::text(format!(
"Elicitation error: {}",
e
))])),
@ -425,23 +420,24 @@ impl ServerHandler for ConformanceServer {
match cx
.peer
.create_elicitation(CreateElicitationRequestParams::FormElicitationParams {
.create_elicitation(ElicitRequestParams::FormElicitationParams {
meta: None,
message: "Please provide values (all have defaults)".into(),
requested_schema: schema,
})
.await
{
Ok(result) => Ok(CallToolResult::success(vec![Content::text(format!(
Ok(result) => Ok(CallToolResult::success(vec![ContentBlock::text(format!(
"Elicitation completed: action={}, content={:?}",
match result.action {
ElicitationAction::Accept => "accept",
ElicitationAction::Decline => "decline",
ElicitationAction::Cancel => "cancel",
_ => "unknown",
},
result.content
))])),
Err(e) => Ok(CallToolResult::error(vec![Content::text(format!(
Err(e) => Ok(CallToolResult::error(vec![ContentBlock::text(format!(
"Elicitation error: {}",
e
))])),
@ -493,22 +489,23 @@ impl ServerHandler for ConformanceServer {
match cx
.peer
.create_elicitation(CreateElicitationRequestParams::FormElicitationParams {
.create_elicitation(ElicitRequestParams::FormElicitationParams {
meta: None,
message: "Test enum schema improvements".into(),
requested_schema: schema,
})
.await
{
Ok(result) => Ok(CallToolResult::success(vec![Content::text(format!(
Ok(result) => Ok(CallToolResult::success(vec![ContentBlock::text(format!(
"Enum elicitation completed: action={}",
match result.action {
ElicitationAction::Accept => "accept",
ElicitationAction::Decline => "decline",
ElicitationAction::Cancel => "cancel",
_ => "unknown",
}
))])),
Err(e) => Ok(CallToolResult::error(vec![Content::text(format!(
Err(e) => Ok(CallToolResult::error(vec![ContentBlock::text(format!(
"Elicitation error: {}",
e
))])),
@ -517,7 +514,7 @@ impl ServerHandler for ConformanceServer {
"json_schema_2020_12_tool" => {
let name = args.get("name").and_then(|v| v.as_str()).unwrap_or("world");
Ok(CallToolResult::success(vec![Content::text(format!(
Ok(CallToolResult::success(vec![ContentBlock::text(format!(
"Hello, {}!",
name
))]))
@ -525,7 +522,7 @@ impl ServerHandler for ConformanceServer {
"test_reconnection" => {
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
Ok(CallToolResult::success(vec![Content::text(
Ok(CallToolResult::success(vec![ContentBlock::text(
"Reconnection test completed",
)]))
}
@ -545,28 +542,12 @@ impl ServerHandler for ConformanceServer {
Ok(ListResourcesResult {
meta: None,
resources: vec![
RawResource {
uri: "test://static-text".into(),
name: "Static Text Resource".into(),
title: None,
description: Some("A static text resource for testing".into()),
mime_type: Some("text/plain".into()),
size: None,
icons: None,
meta: None,
}
.no_annotation(),
RawResource {
uri: "test://static-binary".into(),
name: "Static Binary Resource".into(),
title: None,
description: Some("A static binary/blob resource for testing".into()),
mime_type: Some("image/png".into()),
size: None,
icons: None,
meta: None,
}
.no_annotation(),
Resource::new("test://static-text", "Static Text Resource")
.with_description("A static text resource for testing")
.with_mime_type("text/plain"),
Resource::new("test://static-binary", "Static Binary Resource")
.with_description("A static binary/blob resource for testing")
.with_mime_type("image/png"),
],
next_cursor: None,
})
@ -630,15 +611,9 @@ impl ServerHandler for ConformanceServer {
Ok(ListResourceTemplatesResult {
meta: None,
resource_templates: vec![
RawResourceTemplate {
uri_template: "test://template/{id}/data".into(),
name: "Dynamic Resource".into(),
title: None,
description: Some("A dynamic resource with parameter substitution".into()),
mime_type: Some("application/json".into()),
icons: None,
}
.no_annotation(),
ResourceTemplate::new("test://template/{id}/data", "Dynamic Resource")
.with_description("A dynamic resource with parameter substitution")
.with_mime_type("application/json"),
],
next_cursor: None,
})
@ -711,7 +686,7 @@ impl ServerHandler for ConformanceServer {
) -> Result<GetPromptResult, ErrorData> {
match request.name.as_str() {
"test_simple_prompt" => Ok(GetPromptResult::new(vec![PromptMessage::new_text(
PromptMessageRole::User,
Role::User,
"This is a simple test prompt.",
)])
.with_description("A simple test prompt")),
@ -723,15 +698,15 @@ impl ServerHandler for ConformanceServer {
.and_then(|v| v.as_str())
.unwrap_or("friendly");
Ok(GetPromptResult::new(vec![PromptMessage::new_text(
PromptMessageRole::User,
Role::User,
format!("Please greet {} in a {} style.", name, style),
)])
.with_description("A prompt with arguments"))
}
"test_prompt_with_embedded_resource" => Ok(GetPromptResult::new(vec![
PromptMessage::new_text(PromptMessageRole::User, "Here is a resource:"),
PromptMessage::new_text(Role::User, "Here is a resource:"),
PromptMessage::new_resource(
PromptMessageRole::User,
Role::User,
"test://static-text".into(),
Some("text/plain".into()),
Some("Resource content for prompt".into()),
@ -742,19 +717,10 @@ impl ServerHandler for ConformanceServer {
])
.with_description("A prompt with an embedded resource")),
"test_prompt_with_image" => {
let image_content = RawImageContent {
data: TEST_IMAGE_DATA.into(),
mime_type: "image/png".into(),
meta: None,
};
let image_content = ImageContent::new(TEST_IMAGE_DATA, "image/png");
Ok(GetPromptResult::new(vec![
PromptMessage::new_text(PromptMessageRole::User, "Here is an image:"),
PromptMessage::new(
PromptMessageRole::User,
PromptMessageContent::Image {
image: image_content.no_annotation(),
},
),
PromptMessage::new_text(Role::User, "Here is an image:"),
PromptMessage::new(Role::User, ContentBlock::Image(image_content)),
])
.with_description("A prompt with an image"))
}
@ -787,6 +753,7 @@ impl ServerHandler for ConformanceServer {
vec![prompt_ref.name.clone()]
}
}
_ => vec![],
};
Ok(CompleteResult::new(
CompletionInfo::new(values).map_err(|e| ErrorData::internal_error(e, None))?,