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:
parent
42a1069833
commit
77932141e3
2 changed files with 4321 additions and 3886 deletions
|
|
@ -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"})))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))?,
|
||||
|
|
|
|||
Loading…
Reference in a new issue