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
56 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))?,
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ pub fn task_handler(attr: TokenStream, input: TokenStream) -> syn::Result<TokenS
|
|||
let get_info_fn = quote! {
|
||||
async fn get_task_info(
|
||||
&self,
|
||||
request: rmcp::model::GetTaskInfoParams,
|
||||
request: rmcp::model::GetTaskParams,
|
||||
_context: rmcp::service::RequestContext<rmcp::RoleServer>,
|
||||
) -> Result<rmcp::model::GetTaskResult, McpError> {
|
||||
use rmcp::task_manager::current_timestamp;
|
||||
|
|
@ -145,7 +145,7 @@ pub fn task_handler(attr: TokenStream, input: TokenStream) -> syn::Result<TokenS
|
|||
if let Some(ttl) = completed_result.descriptor.ttl {
|
||||
task = task.with_ttl(ttl);
|
||||
}
|
||||
return Ok(rmcp::model::GetTaskResult { meta: None, task });
|
||||
return Ok(rmcp::model::GetTaskResult::new(task));
|
||||
}
|
||||
|
||||
// If not completed, check running
|
||||
|
|
@ -158,7 +158,7 @@ pub fn task_handler(attr: TokenStream, input: TokenStream) -> syn::Result<TokenS
|
|||
timestamp.clone(),
|
||||
timestamp,
|
||||
);
|
||||
return Ok(rmcp::model::GetTaskResult { meta: None, task });
|
||||
return Ok(rmcp::model::GetTaskResult::new(task));
|
||||
}
|
||||
|
||||
Err(McpError::resource_not_found(format!("task not found: {}", task_id), None))
|
||||
|
|
@ -171,7 +171,7 @@ pub fn task_handler(attr: TokenStream, input: TokenStream) -> syn::Result<TokenS
|
|||
let get_result_fn = quote! {
|
||||
async fn get_task_result(
|
||||
&self,
|
||||
request: rmcp::model::GetTaskResultParams,
|
||||
request: rmcp::model::GetTaskPayloadParams,
|
||||
_context: rmcp::service::RequestContext<rmcp::RoleServer>,
|
||||
) -> Result<rmcp::model::GetTaskPayloadResult, McpError> {
|
||||
use std::time::Duration;
|
||||
|
|
@ -242,7 +242,7 @@ pub fn task_handler(attr: TokenStream, input: TokenStream) -> syn::Result<TokenS
|
|||
timestamp.clone(),
|
||||
timestamp,
|
||||
);
|
||||
return Ok(rmcp::model::CancelTaskResult { meta: None, task });
|
||||
return Ok(rmcp::model::CancelTaskResult::new(task));
|
||||
}
|
||||
|
||||
// If already completed, signal it's not cancellable
|
||||
|
|
|
|||
|
|
@ -26,10 +26,10 @@ impl<H: ClientHandler> Service<RoleClient> for H {
|
|||
.list_roots(context)
|
||||
.await
|
||||
.map(ClientResult::ListRootsResult),
|
||||
ServerRequest::CreateElicitationRequest(request) => self
|
||||
ServerRequest::ElicitRequest(request) => self
|
||||
.create_elicitation(request.params, context)
|
||||
.await
|
||||
.map(ClientResult::CreateElicitationResult),
|
||||
.map(ClientResult::ElicitResult),
|
||||
ServerRequest::CustomRequest(request) => self
|
||||
.on_custom_request(request, context)
|
||||
.await
|
||||
|
|
@ -64,10 +64,13 @@ impl<H: ClientHandler> Service<RoleClient> for H {
|
|||
ServerNotification::PromptListChangedNotification(_notification_no_param) => {
|
||||
self.on_prompt_list_changed(context).await
|
||||
}
|
||||
ServerNotification::ElicitationCompletionNotification(notification) => {
|
||||
ServerNotification::ElicitationCompleteNotification(notification) => {
|
||||
self.on_url_elicitation_notification_complete(notification.params, context)
|
||||
.await
|
||||
}
|
||||
ServerNotification::TaskStatusNotification(notification) => {
|
||||
self.on_task_status(notification.params, context).await
|
||||
}
|
||||
ServerNotification::CustomNotification(notification) => {
|
||||
self.on_custom_notification(notification, context).await
|
||||
}
|
||||
|
|
@ -125,7 +128,7 @@ pub trait ClientHandler: Sized + Send + Sync + 'static {
|
|||
///
|
||||
/// # Example
|
||||
/// ```rust,ignore
|
||||
/// use rmcp::model::CreateElicitationRequestParam;
|
||||
/// use rmcp::model::ElicitRequestParams;
|
||||
/// use rmcp::{
|
||||
/// model::ErrorData as McpError,
|
||||
/// model::*,
|
||||
|
|
@ -136,23 +139,23 @@ pub trait ClientHandler: Sized + Send + Sync + 'static {
|
|||
/// impl ClientHandler for MyClient {
|
||||
/// async fn create_elicitation(
|
||||
/// &self,
|
||||
/// request: CreateElicitationRequestParam,
|
||||
/// request: ElicitRequestParams,
|
||||
/// context: RequestContext<RoleClient>,
|
||||
/// ) -> Result<CreateElicitationResult, McpError> {
|
||||
/// ) -> Result<ElicitResult, McpError> {
|
||||
/// match request {
|
||||
/// CreateElicitationRequestParam::FormElicitationParam {meta, message, requested_schema,} => {
|
||||
/// ElicitRequestParams::FormElicitationParam {meta, message, requested_schema,} => {
|
||||
/// // Display message to user and collect input according to requested_schema
|
||||
/// let user_input = get_user_input(message, requested_schema).await?;
|
||||
/// Ok(CreateElicitationResult {
|
||||
/// Ok(ElicitResult {
|
||||
/// action: ElicitationAction::Accept,
|
||||
/// content: Some(user_input),
|
||||
/// meta: None,
|
||||
/// })
|
||||
/// }
|
||||
/// CreateElicitationRequestParam::UrlElicitationParam {meta, message, url, elicitation_id,} => {
|
||||
/// ElicitRequestParams::UrlElicitationParam {meta, message, url, elicitation_id,} => {
|
||||
/// // Open URL in browser for user to complete elicitation
|
||||
/// open_url_in_browser(url).await?;
|
||||
/// Ok(CreateElicitationResult {
|
||||
/// Ok(ElicitResult {
|
||||
/// action: ElicitationAction::Accept,
|
||||
/// content: None,
|
||||
/// meta: None,
|
||||
|
|
@ -164,13 +167,12 @@ pub trait ClientHandler: Sized + Send + Sync + 'static {
|
|||
/// ```
|
||||
fn create_elicitation(
|
||||
&self,
|
||||
request: CreateElicitationRequestParams,
|
||||
request: ElicitRequestParams,
|
||||
context: RequestContext<RoleClient>,
|
||||
) -> impl Future<Output = Result<CreateElicitationResult, McpError>> + MaybeSendFuture + '_
|
||||
{
|
||||
) -> impl Future<Output = Result<ElicitResult, McpError>> + MaybeSendFuture + '_ {
|
||||
// Default implementation declines all requests - real clients should override this
|
||||
let _ = (request, context);
|
||||
std::future::ready(Ok(CreateElicitationResult {
|
||||
std::future::ready(Ok(ElicitResult {
|
||||
action: ElicitationAction::Decline,
|
||||
content: None,
|
||||
meta: None,
|
||||
|
|
@ -245,6 +247,13 @@ pub trait ClientHandler: Sized + Send + Sync + 'static {
|
|||
) -> impl Future<Output = ()> + MaybeSendFuture + '_ {
|
||||
std::future::ready(())
|
||||
}
|
||||
fn on_task_status(
|
||||
&self,
|
||||
params: TaskStatusNotificationParam,
|
||||
context: NotificationContext<RoleClient>,
|
||||
) -> impl Future<Output = ()> + MaybeSendFuture + '_ {
|
||||
std::future::ready(())
|
||||
}
|
||||
fn on_custom_notification(
|
||||
&self,
|
||||
notification: CustomNotification,
|
||||
|
|
@ -283,22 +292,24 @@ macro_rules! impl_client_handler_for_wrapper {
|
|||
&self,
|
||||
params: CreateMessageRequestParams,
|
||||
context: RequestContext<RoleClient>,
|
||||
) -> impl Future<Output = Result<CreateMessageResult, McpError>> + MaybeSendFuture + '_ {
|
||||
) -> impl Future<Output = Result<CreateMessageResult, McpError>> + MaybeSendFuture + '_
|
||||
{
|
||||
(**self).create_message(params, context)
|
||||
}
|
||||
|
||||
fn list_roots(
|
||||
&self,
|
||||
context: RequestContext<RoleClient>,
|
||||
) -> impl Future<Output = Result<ListRootsResult, McpError>> + MaybeSendFuture + '_ {
|
||||
) -> impl Future<Output = Result<ListRootsResult, McpError>> + MaybeSendFuture + '_
|
||||
{
|
||||
(**self).list_roots(context)
|
||||
}
|
||||
|
||||
fn create_elicitation(
|
||||
&self,
|
||||
request: CreateElicitationRequestParams,
|
||||
request: ElicitRequestParams,
|
||||
context: RequestContext<RoleClient>,
|
||||
) -> impl Future<Output = Result<CreateElicitationResult, McpError>> + MaybeSendFuture + '_ {
|
||||
) -> impl Future<Output = Result<ElicitResult, McpError>> + MaybeSendFuture + '_ {
|
||||
(**self).create_elicitation(request, context)
|
||||
}
|
||||
|
||||
|
|
@ -363,6 +374,14 @@ macro_rules! impl_client_handler_for_wrapper {
|
|||
(**self).on_prompt_list_changed(context)
|
||||
}
|
||||
|
||||
fn on_task_status(
|
||||
&self,
|
||||
params: TaskStatusNotificationParam,
|
||||
context: NotificationContext<RoleClient>,
|
||||
) -> impl Future<Output = ()> + MaybeSendFuture + '_ {
|
||||
(**self).on_task_status(params, context)
|
||||
}
|
||||
|
||||
fn on_custom_notification(
|
||||
&self,
|
||||
notification: CustomNotification,
|
||||
|
|
|
|||
|
|
@ -117,11 +117,11 @@ impl<H: ServerHandler> Service<RoleServer> for H {
|
|||
.list_tasks(request.params, context)
|
||||
.await
|
||||
.map(ServerResult::ListTasksResult),
|
||||
ClientRequest::GetTaskInfoRequest(request) => self
|
||||
ClientRequest::GetTaskRequest(request) => self
|
||||
.get_task_info(request.params, context)
|
||||
.await
|
||||
.map(ServerResult::GetTaskResult),
|
||||
ClientRequest::GetTaskResultRequest(request) => self
|
||||
ClientRequest::GetTaskPayloadRequest(request) => self
|
||||
.get_task_result(request.params, context)
|
||||
.await
|
||||
.map(ServerResult::GetTaskPayloadResult),
|
||||
|
|
@ -161,6 +161,9 @@ impl<H: ServerHandler> Service<RoleServer> for H {
|
|||
ClientNotification::RootsListChangedNotification(_notification) => {
|
||||
self.on_roots_list_changed(context).await
|
||||
}
|
||||
ClientNotification::TaskStatusNotification(notification) => {
|
||||
self.on_task_status(notification.params, context).await
|
||||
}
|
||||
ClientNotification::CustomNotification(notification) => {
|
||||
self.on_custom_notification(notification, context).await
|
||||
}
|
||||
|
|
@ -359,6 +362,13 @@ macro_rules! server_handler_methods {
|
|||
) -> impl Future<Output = ()> + MaybeSendFuture + '_ {
|
||||
std::future::ready(())
|
||||
}
|
||||
fn on_task_status(
|
||||
&self,
|
||||
params: TaskStatusNotificationParam,
|
||||
context: NotificationContext<RoleServer>,
|
||||
) -> impl Future<Output = ()> + MaybeSendFuture + '_ {
|
||||
std::future::ready(())
|
||||
}
|
||||
fn on_custom_notification(
|
||||
&self,
|
||||
notification: CustomNotification,
|
||||
|
|
@ -382,20 +392,20 @@ macro_rules! server_handler_methods {
|
|||
|
||||
fn get_task_info(
|
||||
&self,
|
||||
request: GetTaskInfoParams,
|
||||
request: GetTaskParams,
|
||||
context: RequestContext<RoleServer>,
|
||||
) -> impl Future<Output = Result<GetTaskResult, McpError>> + MaybeSendFuture + '_ {
|
||||
let _ = (request, context);
|
||||
std::future::ready(Err(McpError::method_not_found::<GetTaskInfoMethod>()))
|
||||
std::future::ready(Err(McpError::method_not_found::<GetTaskMethod>()))
|
||||
}
|
||||
|
||||
fn get_task_result(
|
||||
&self,
|
||||
request: GetTaskResultParams,
|
||||
request: GetTaskPayloadParams,
|
||||
context: RequestContext<RoleServer>,
|
||||
) -> impl Future<Output = Result<GetTaskPayloadResult, McpError>> + MaybeSendFuture + '_ {
|
||||
let _ = (request, context);
|
||||
std::future::ready(Err(McpError::method_not_found::<GetTaskResultMethod>()))
|
||||
std::future::ready(Err(McpError::method_not_found::<GetTaskPayloadMethod>()))
|
||||
}
|
||||
|
||||
fn cancel_task(
|
||||
|
|
@ -578,6 +588,14 @@ macro_rules! impl_server_handler_for_wrapper {
|
|||
(**self).on_roots_list_changed(context)
|
||||
}
|
||||
|
||||
fn on_task_status(
|
||||
&self,
|
||||
params: TaskStatusNotificationParam,
|
||||
context: NotificationContext<RoleServer>,
|
||||
) -> impl Future<Output = ()> + MaybeSendFuture + '_ {
|
||||
(**self).on_task_status(params, context)
|
||||
}
|
||||
|
||||
fn on_custom_notification(
|
||||
&self,
|
||||
notification: CustomNotification,
|
||||
|
|
@ -600,7 +618,7 @@ macro_rules! impl_server_handler_for_wrapper {
|
|||
|
||||
fn get_task_info(
|
||||
&self,
|
||||
request: GetTaskInfoParams,
|
||||
request: GetTaskParams,
|
||||
context: RequestContext<RoleServer>,
|
||||
) -> impl Future<Output = Result<GetTaskResult, McpError>> + MaybeSendFuture + '_ {
|
||||
(**self).get_task_info(request, context)
|
||||
|
|
@ -608,7 +626,7 @@ macro_rules! impl_server_handler_for_wrapper {
|
|||
|
||||
fn get_task_result(
|
||||
&self,
|
||||
request: GetTaskResultParams,
|
||||
request: GetTaskPayloadParams,
|
||||
context: RequestContext<RoleServer>,
|
||||
) -> impl Future<Output = Result<GetTaskPayloadResult, McpError>> + MaybeSendFuture + '_ {
|
||||
(**self).get_task_result(request, context)
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ impl IntoGetPromptResult for Vec<PromptMessage> {
|
|||
Ok(GetPromptResult {
|
||||
description: None,
|
||||
messages: self,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ use crate::{
|
|||
tool::{CallToolHandler, DynCallToolHandler, ToolCallContext},
|
||||
tool_name_validation::validate_and_warn_tool_name,
|
||||
},
|
||||
model::{CallToolResult, Content, ErrorCode, Tool, ToolAnnotations},
|
||||
model::{CallToolResult, ContentBlock, ErrorCode, Tool, ToolAnnotations},
|
||||
service::{MaybeBoxFuture, MaybeSend},
|
||||
};
|
||||
|
||||
|
|
@ -149,7 +149,9 @@ fn into_tool_argument_error(error: crate::ErrorData) -> Result<CallToolResult, c
|
|||
.message
|
||||
.starts_with(TOOL_ARGUMENT_DESERIALIZATION_ERROR_PREFIX)
|
||||
{
|
||||
return Ok(CallToolResult::error(vec![Content::text(error.message)]));
|
||||
return Ok(CallToolResult::error(vec![ContentBlock::text(
|
||||
error.message,
|
||||
)]));
|
||||
}
|
||||
|
||||
Err(error)
|
||||
|
|
@ -680,7 +682,7 @@ mod tests {
|
|||
let text = result
|
||||
.content
|
||||
.first()
|
||||
.and_then(|content| content.raw.as_text())
|
||||
.and_then(|content| content.as_text())
|
||||
.map(|text| text.text.as_str())
|
||||
.expect("tool error result should include text");
|
||||
assert!(text.contains("failed to deserialize parameters"));
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ pub struct ToolCallContext<'s, S> {
|
|||
pub service: &'s S,
|
||||
pub name: Cow<'static, str>,
|
||||
pub arguments: Option<JsonObject>,
|
||||
pub task: Option<JsonObject>,
|
||||
pub task: Option<crate::model::TaskMetadata>,
|
||||
}
|
||||
|
||||
impl<'s, S> ToolCallContext<'s, S> {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,13 +1,15 @@
|
|||
use std::ops::{Deref, DerefMut};
|
||||
//! Annotations for content blocks and resources.
|
||||
//!
|
||||
//! The `Annotations` struct carries optional hints about audience, priority, and freshness.
|
||||
//! Individual content/resource types embed `annotations: Option<Annotations>` directly.
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{
|
||||
RawAudioContent, RawContent, RawEmbeddedResource, RawImageContent, RawResource,
|
||||
RawResourceTemplate, RawTextContent, Role,
|
||||
};
|
||||
use super::Role;
|
||||
|
||||
/// Optional annotations for the client. The client can use annotations to inform how objects are
|
||||
/// used or displayed.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
|
|
@ -35,192 +37,23 @@ impl Annotations {
|
|||
audience: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
|
||||
pub struct Annotated<T: AnnotateAble> {
|
||||
#[serde(flatten)]
|
||||
pub raw: T,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub annotations: Option<Annotations>,
|
||||
}
|
||||
pub fn with_audience(mut self, audience: Vec<Role>) -> Self {
|
||||
self.audience = Some(audience);
|
||||
self
|
||||
}
|
||||
|
||||
impl<T: AnnotateAble> Deref for Annotated<T> {
|
||||
type Target = T;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.raw
|
||||
pub fn with_priority(mut self, priority: f32) -> Self {
|
||||
self.priority = Some(priority);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AnnotateAble> DerefMut for Annotated<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.raw
|
||||
pub fn with_timestamp(mut self, timestamp: DateTime<Utc>) -> Self {
|
||||
self.last_modified = Some(timestamp);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AnnotateAble> Annotated<T> {
|
||||
pub fn new(raw: T, annotations: Option<Annotations>) -> Self {
|
||||
Self { raw, annotations }
|
||||
}
|
||||
pub fn remove_annotation(&mut self) -> Option<Annotations> {
|
||||
self.annotations.take()
|
||||
}
|
||||
pub fn audience(&self) -> Option<&Vec<Role>> {
|
||||
self.annotations.as_ref().and_then(|a| a.audience.as_ref())
|
||||
}
|
||||
pub fn priority(&self) -> Option<f32> {
|
||||
self.annotations.as_ref().and_then(|a| a.priority)
|
||||
}
|
||||
pub fn timestamp(&self) -> Option<DateTime<Utc>> {
|
||||
self.annotations.as_ref().and_then(|a| a.last_modified)
|
||||
}
|
||||
pub fn with_audience(self, audience: Vec<Role>) -> Annotated<T>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
if let Some(annotations) = self.annotations {
|
||||
Annotated {
|
||||
raw: self.raw,
|
||||
annotations: Some(Annotations {
|
||||
audience: Some(audience),
|
||||
..annotations
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
Annotated {
|
||||
raw: self.raw,
|
||||
annotations: Some(Annotations {
|
||||
audience: Some(audience),
|
||||
priority: None,
|
||||
last_modified: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn with_priority(self, priority: f32) -> Annotated<T>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
if let Some(annotations) = self.annotations {
|
||||
Annotated {
|
||||
raw: self.raw,
|
||||
annotations: Some(Annotations {
|
||||
priority: Some(priority),
|
||||
..annotations
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
Annotated {
|
||||
raw: self.raw,
|
||||
annotations: Some(Annotations {
|
||||
priority: Some(priority),
|
||||
last_modified: None,
|
||||
audience: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn with_timestamp(self, timestamp: DateTime<Utc>) -> Annotated<T>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
if let Some(annotations) = self.annotations {
|
||||
Annotated {
|
||||
raw: self.raw,
|
||||
annotations: Some(Annotations {
|
||||
last_modified: Some(timestamp),
|
||||
..annotations
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
Annotated {
|
||||
raw: self.raw,
|
||||
annotations: Some(Annotations {
|
||||
last_modified: Some(timestamp),
|
||||
priority: None,
|
||||
audience: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn with_timestamp_now(self) -> Annotated<T>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.with_timestamp(Utc::now())
|
||||
}
|
||||
}
|
||||
|
||||
mod sealed {
|
||||
pub trait Sealed {}
|
||||
}
|
||||
macro_rules! annotate {
|
||||
($T: ident) => {
|
||||
impl sealed::Sealed for $T {}
|
||||
impl AnnotateAble for $T {}
|
||||
};
|
||||
}
|
||||
|
||||
annotate!(RawContent);
|
||||
annotate!(RawTextContent);
|
||||
annotate!(RawImageContent);
|
||||
annotate!(RawAudioContent);
|
||||
annotate!(RawEmbeddedResource);
|
||||
annotate!(RawResource);
|
||||
annotate!(RawResourceTemplate);
|
||||
|
||||
pub trait AnnotateAble: sealed::Sealed {
|
||||
fn optional_annotate(self, annotations: Option<Annotations>) -> Annotated<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Annotated::new(self, annotations)
|
||||
}
|
||||
fn annotate(self, annotations: Annotations) -> Annotated<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Annotated::new(self, Some(annotations))
|
||||
}
|
||||
fn no_annotation(self) -> Annotated<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Annotated::new(self, None)
|
||||
}
|
||||
fn with_audience(self, audience: Vec<Role>) -> Annotated<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.annotate(Annotations {
|
||||
audience: Some(audience),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
fn with_priority(self, priority: f32) -> Annotated<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.annotate(Annotations {
|
||||
priority: Some(priority),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
fn with_timestamp(self, timestamp: DateTime<Utc>) -> Annotated<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.annotate(Annotations {
|
||||
last_modified: Some(timestamp),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
fn with_timestamp_now(self) -> Annotated<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
pub fn with_timestamp_now(self) -> Self {
|
||||
self.with_timestamp(Utc::now())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ pub type ExtensionCapabilities = BTreeMap<String, JsonObject>;
|
|||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
|
||||
#[non_exhaustive]
|
||||
pub struct PromptsCapability {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub list_changed: Option<bool>,
|
||||
|
|
@ -43,7 +43,7 @@ pub struct PromptsCapability {
|
|||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
|
||||
#[non_exhaustive]
|
||||
pub struct ResourcesCapability {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub subscribe: Option<bool>,
|
||||
|
|
@ -54,7 +54,7 @@ pub struct ResourcesCapability {
|
|||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
|
||||
#[non_exhaustive]
|
||||
pub struct ToolsCapability {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub list_changed: Option<bool>,
|
||||
|
|
@ -66,7 +66,7 @@ pub struct ToolsCapability {
|
|||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
|
||||
#[non_exhaustive]
|
||||
pub struct RootsCapabilities {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub list_changed: Option<bool>,
|
||||
|
|
@ -76,7 +76,7 @@ pub struct RootsCapabilities {
|
|||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
|
||||
#[non_exhaustive]
|
||||
pub struct TasksCapability {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub requests: Option<TaskRequestsCapability>,
|
||||
|
|
@ -90,7 +90,7 @@ pub struct TasksCapability {
|
|||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
|
||||
#[non_exhaustive]
|
||||
pub struct TaskRequestsCapability {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sampling: Option<SamplingTaskCapability>,
|
||||
|
|
@ -106,7 +106,7 @@ pub struct TaskRequestsCapability {
|
|||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
|
||||
#[non_exhaustive]
|
||||
pub struct SamplingTaskCapability {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub create_message: Option<JsonObject>,
|
||||
|
|
@ -115,7 +115,7 @@ pub struct SamplingTaskCapability {
|
|||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
|
||||
#[non_exhaustive]
|
||||
pub struct ElicitationTaskCapability {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub create: Option<JsonObject>,
|
||||
|
|
@ -124,7 +124,7 @@ pub struct ElicitationTaskCapability {
|
|||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
|
||||
#[non_exhaustive]
|
||||
pub struct ToolsTaskCapability {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub call: Option<JsonObject>,
|
||||
|
|
@ -205,7 +205,7 @@ impl TasksCapability {
|
|||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
|
||||
#[non_exhaustive]
|
||||
pub struct FormElicitationCapability {
|
||||
/// Whether the client supports JSON Schema validation for elicitation responses.
|
||||
/// When true, the client will validate user input against the requested_schema
|
||||
|
|
@ -214,19 +214,36 @@ pub struct FormElicitationCapability {
|
|||
pub schema_validation: Option<bool>,
|
||||
}
|
||||
|
||||
impl FormElicitationCapability {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn with_schema_validation(mut self, enabled: bool) -> Self {
|
||||
self.schema_validation = Some(enabled);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Capability for URL mode elicitation.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
|
||||
#[non_exhaustive]
|
||||
pub struct UrlElicitationCapability {}
|
||||
|
||||
impl UrlElicitationCapability {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Elicitation allows servers to request interactive input from users during tool execution.
|
||||
/// This capability indicates that a client can handle elicitation requests and present
|
||||
/// appropriate UI to users for collecting the requested information.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
|
||||
#[non_exhaustive]
|
||||
pub struct ElicitationCapability {
|
||||
/// Whether client supports form-based elicitation.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
|
@ -236,6 +253,22 @@ pub struct ElicitationCapability {
|
|||
pub url: Option<UrlElicitationCapability>,
|
||||
}
|
||||
|
||||
impl ElicitationCapability {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn with_form(mut self, form: FormElicitationCapability) -> Self {
|
||||
self.form = Some(form);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_url(mut self, url: UrlElicitationCapability) -> Self {
|
||||
self.url = Some(url);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Sampling capability with optional sub-capabilities (SEP-1577).
|
||||
///
|
||||
/// Deprecated by SEP-2577; remains functional and will be removed in a future
|
||||
|
|
@ -244,7 +277,7 @@ pub struct ElicitationCapability {
|
|||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
|
||||
#[non_exhaustive]
|
||||
pub struct SamplingCapability {
|
||||
/// Support for `tools` and `toolChoice` parameters
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
|
|
|||
|
|
@ -1,78 +1,177 @@
|
|||
//! Content sent around agents, extensions, and LLMs
|
||||
//! The various content types can be display to humans but also understood by models
|
||||
//! They include optional annotations used to help inform agent usage
|
||||
//! Content types that flow between agents, tools, prompts, and LLMs.
|
||||
//!
|
||||
//! The core union is [`ContentBlock`] (text | image | audio | resource_link | resource),
|
||||
//! matching the MCP 2025-11-25 `ContentBlock` definition. Each variant carries optional
|
||||
//! [`Annotations`] and `_meta` inline.
|
||||
//!
|
||||
//! [`SamplingMessageContentBlock`] extends the union with `tool_use` and `tool_result`
|
||||
//! variants for sampling messages (SEP-1577).
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
|
||||
use super::{AnnotateAble, Annotated, resource::ResourceContents};
|
||||
use super::{Annotations, Meta, resource::ResourceContents};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Flat content structs
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Text content block (spec `TextContent`).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
|
||||
pub struct RawTextContent {
|
||||
#[non_exhaustive]
|
||||
pub struct TextContent {
|
||||
/// The text content of the message.
|
||||
pub text: String,
|
||||
/// Optional protocol-level metadata for this content block
|
||||
/// Optional protocol-level metadata for this content block.
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<super::Meta>,
|
||||
}
|
||||
pub type TextContent = Annotated<RawTextContent>;
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
|
||||
pub struct RawImageContent {
|
||||
/// The base64-encoded image
|
||||
pub data: String,
|
||||
pub mime_type: String,
|
||||
/// Optional protocol-level metadata for this content block
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<super::Meta>,
|
||||
pub meta: Option<Meta>,
|
||||
/// Optional annotations describing how the client should use this content.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub annotations: Option<Annotations>,
|
||||
}
|
||||
|
||||
pub type ImageContent = Annotated<RawImageContent>;
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
|
||||
pub struct RawEmbeddedResource {
|
||||
/// Optional protocol-level metadata for this content block
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<super::Meta>,
|
||||
pub resource: ResourceContents,
|
||||
}
|
||||
|
||||
impl RawEmbeddedResource {
|
||||
/// Create a new RawEmbeddedResource.
|
||||
pub fn new(resource: ResourceContents) -> Self {
|
||||
impl TextContent {
|
||||
pub fn new(text: impl Into<String>) -> Self {
|
||||
Self {
|
||||
text: text.into(),
|
||||
meta: None,
|
||||
resource,
|
||||
annotations: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_meta(mut self, meta: Meta) -> Self {
|
||||
self.meta = Some(meta);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_annotations(mut self, annotations: Annotations) -> Self {
|
||||
self.annotations = Some(annotations);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub type EmbeddedResource = Annotated<RawEmbeddedResource>;
|
||||
/// Image content with base64-encoded data (spec `ImageContent`).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[non_exhaustive]
|
||||
pub struct ImageContent {
|
||||
/// The base64-encoded image data.
|
||||
pub data: String,
|
||||
/// The MIME type of the image (e.g. `image/png`).
|
||||
pub mime_type: String,
|
||||
/// Optional protocol-level metadata for this content block.
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<Meta>,
|
||||
/// Optional annotations describing how the client should use this content.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub annotations: Option<Annotations>,
|
||||
}
|
||||
|
||||
impl ImageContent {
|
||||
pub fn new(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
|
||||
Self {
|
||||
data: data.into(),
|
||||
mime_type: mime_type.into(),
|
||||
meta: None,
|
||||
annotations: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_meta(mut self, meta: Meta) -> Self {
|
||||
self.meta = Some(meta);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_annotations(mut self, annotations: Annotations) -> Self {
|
||||
self.annotations = Some(annotations);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Audio content with base64-encoded data (spec `AudioContent`).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[non_exhaustive]
|
||||
pub struct AudioContent {
|
||||
/// The base64-encoded audio data.
|
||||
pub data: String,
|
||||
/// The MIME type of the audio (e.g. `audio/wav`).
|
||||
pub mime_type: String,
|
||||
/// Optional protocol-level metadata for this content block.
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<Meta>,
|
||||
/// Optional annotations describing how the client should use this content.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub annotations: Option<Annotations>,
|
||||
}
|
||||
|
||||
impl AudioContent {
|
||||
pub fn new(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
|
||||
Self {
|
||||
data: data.into(),
|
||||
mime_type: mime_type.into(),
|
||||
meta: None,
|
||||
annotations: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_meta(mut self, meta: Meta) -> Self {
|
||||
self.meta = Some(meta);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_annotations(mut self, annotations: Annotations) -> Self {
|
||||
self.annotations = Some(annotations);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Embedded resource content (spec `EmbeddedResource`).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[non_exhaustive]
|
||||
pub struct EmbeddedResource {
|
||||
/// The embedded resource contents (text or blob).
|
||||
pub resource: ResourceContents,
|
||||
/// Optional protocol-level metadata for this content block.
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<Meta>,
|
||||
/// Optional annotations describing how the client should use this content.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub annotations: Option<Annotations>,
|
||||
}
|
||||
|
||||
impl EmbeddedResource {
|
||||
pub fn new(resource: ResourceContents) -> Self {
|
||||
Self {
|
||||
resource,
|
||||
meta: None,
|
||||
annotations: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_text(&self) -> String {
|
||||
match &self.resource {
|
||||
ResourceContents::TextResourceContents { text, .. } => text.clone(),
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
|
||||
pub struct RawAudioContent {
|
||||
pub data: String,
|
||||
pub mime_type: String,
|
||||
}
|
||||
pub fn with_meta(mut self, meta: Meta) -> Self {
|
||||
self.meta = Some(meta);
|
||||
self
|
||||
}
|
||||
|
||||
pub type AudioContent = Annotated<RawAudioContent>;
|
||||
pub fn with_annotations(mut self, annotations: Annotations) -> Self {
|
||||
self.annotations = Some(annotations);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Tool call request from assistant (SEP-1577).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
|
|
@ -80,15 +179,11 @@ pub type AudioContent = Annotated<RawAudioContent>;
|
|||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[non_exhaustive]
|
||||
pub struct ToolUseContent {
|
||||
/// Unique identifier for this tool call
|
||||
pub id: String,
|
||||
/// Name of the tool to call
|
||||
pub name: String,
|
||||
/// Input arguments for the tool
|
||||
pub input: super::JsonObject,
|
||||
/// Optional metadata (preserved for caching)
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<super::Meta>,
|
||||
pub meta: Option<Meta>,
|
||||
}
|
||||
|
||||
/// Tool execution result in user message (SEP-1577).
|
||||
|
|
@ -97,18 +192,12 @@ pub struct ToolUseContent {
|
|||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[non_exhaustive]
|
||||
pub struct ToolResultContent {
|
||||
/// Optional metadata
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<super::Meta>,
|
||||
/// ID of the corresponding tool use
|
||||
pub meta: Option<Meta>,
|
||||
pub tool_use_id: String,
|
||||
/// Content blocks returned by the tool
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub content: Vec<Content>,
|
||||
/// Optional structured result
|
||||
pub content: Vec<ContentBlock>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub structured_content: Option<super::JsonObject>,
|
||||
/// Whether tool execution failed
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub is_error: Option<bool>,
|
||||
}
|
||||
|
|
@ -125,7 +214,7 @@ impl ToolUseContent {
|
|||
}
|
||||
|
||||
impl ToolResultContent {
|
||||
pub fn new(tool_use_id: impl Into<String>, content: Vec<Content>) -> Self {
|
||||
pub fn new(tool_use_id: impl Into<String>, content: Vec<ContentBlock>) -> Self {
|
||||
Self {
|
||||
meta: None,
|
||||
tool_use_id: tool_use_id.into(),
|
||||
|
|
@ -135,7 +224,7 @@ impl ToolResultContent {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn error(tool_use_id: impl Into<String>, content: Vec<Content>) -> Self {
|
||||
pub fn error(tool_use_id: impl Into<String>, content: Vec<ContentBlock>) -> Self {
|
||||
Self {
|
||||
meta: None,
|
||||
tool_use_id: tool_use_id.into(),
|
||||
|
|
@ -146,21 +235,26 @@ impl ToolResultContent {
|
|||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ContentBlock — the unified content union (spec `ContentBlock`)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Unified content block union (spec `ContentBlock`).
|
||||
///
|
||||
/// `text | image | audio | resource_link | resource`
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")]
|
||||
pub enum RawContent {
|
||||
Text(RawTextContent),
|
||||
Image(RawImageContent),
|
||||
Resource(RawEmbeddedResource),
|
||||
Audio(RawAudioContent),
|
||||
ResourceLink(super::resource::RawResource),
|
||||
#[non_exhaustive]
|
||||
pub enum ContentBlock {
|
||||
Text(TextContent),
|
||||
Image(ImageContent),
|
||||
Audio(AudioContent),
|
||||
Resource(EmbeddedResource),
|
||||
ResourceLink(super::resource::Resource),
|
||||
}
|
||||
|
||||
pub type Content = Annotated<RawContent>;
|
||||
|
||||
impl RawContent {
|
||||
impl ContentBlock {
|
||||
pub fn json<S: Serialize>(json: S) -> Result<Self, crate::ErrorData> {
|
||||
let json = serde_json::to_string(&json).map_err(|e| {
|
||||
crate::ErrorData::internal_error(
|
||||
|
|
@ -170,129 +264,106 @@ impl RawContent {
|
|||
)),
|
||||
)
|
||||
})?;
|
||||
Ok(RawContent::text(json))
|
||||
Ok(ContentBlock::text(json))
|
||||
}
|
||||
|
||||
pub fn text<S: Into<String>>(text: S) -> Self {
|
||||
RawContent::Text(RawTextContent {
|
||||
text: text.into(),
|
||||
meta: None,
|
||||
})
|
||||
pub fn text(text: impl Into<String>) -> Self {
|
||||
ContentBlock::Text(TextContent::new(text))
|
||||
}
|
||||
|
||||
pub fn image<S: Into<String>, T: Into<String>>(data: S, mime_type: T) -> Self {
|
||||
RawContent::Image(RawImageContent {
|
||||
data: data.into(),
|
||||
mime_type: mime_type.into(),
|
||||
meta: None,
|
||||
})
|
||||
pub fn image(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
|
||||
ContentBlock::Image(ImageContent::new(data, mime_type))
|
||||
}
|
||||
|
||||
pub fn audio(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
|
||||
ContentBlock::Audio(AudioContent::new(data, mime_type))
|
||||
}
|
||||
|
||||
pub fn resource(resource: ResourceContents) -> Self {
|
||||
RawContent::Resource(RawEmbeddedResource {
|
||||
meta: None,
|
||||
resource,
|
||||
})
|
||||
ContentBlock::Resource(EmbeddedResource::new(resource))
|
||||
}
|
||||
|
||||
pub fn embedded_text<S: Into<String>, T: Into<String>>(uri: S, content: T) -> Self {
|
||||
RawContent::Resource(RawEmbeddedResource {
|
||||
meta: None,
|
||||
resource: ResourceContents::TextResourceContents {
|
||||
pub fn embedded_text(uri: impl Into<String>, content: impl Into<String>) -> Self {
|
||||
ContentBlock::Resource(EmbeddedResource::new(
|
||||
ResourceContents::TextResourceContents {
|
||||
uri: uri.into(),
|
||||
mime_type: Some("text".to_string()),
|
||||
text: content.into(),
|
||||
meta: None,
|
||||
},
|
||||
})
|
||||
))
|
||||
}
|
||||
|
||||
/// Get the text content if this is a TextContent variant
|
||||
pub fn as_text(&self) -> Option<&RawTextContent> {
|
||||
pub fn resource_link(resource: super::resource::Resource) -> Self {
|
||||
ContentBlock::ResourceLink(resource)
|
||||
}
|
||||
|
||||
pub fn as_text(&self) -> Option<&TextContent> {
|
||||
match self {
|
||||
RawContent::Text(text) => Some(text),
|
||||
ContentBlock::Text(text) => Some(text),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the image content if this is an ImageContent variant
|
||||
pub fn as_image(&self) -> Option<&RawImageContent> {
|
||||
pub fn as_image(&self) -> Option<&ImageContent> {
|
||||
match self {
|
||||
RawContent::Image(image) => Some(image),
|
||||
ContentBlock::Image(image) => Some(image),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the resource content if this is an ImageContent variant
|
||||
pub fn as_resource(&self) -> Option<&RawEmbeddedResource> {
|
||||
pub fn as_resource(&self) -> Option<&EmbeddedResource> {
|
||||
match self {
|
||||
RawContent::Resource(resource) => Some(resource),
|
||||
ContentBlock::Resource(resource) => Some(resource),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the resource link if this is a ResourceLink variant
|
||||
pub fn as_resource_link(&self) -> Option<&super::resource::RawResource> {
|
||||
pub fn as_resource_link(&self) -> Option<&super::resource::Resource> {
|
||||
match self {
|
||||
RawContent::ResourceLink(link) => Some(link),
|
||||
ContentBlock::ResourceLink(link) => Some(link),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a resource link content
|
||||
pub fn resource_link(resource: super::resource::RawResource) -> Self {
|
||||
RawContent::ResourceLink(resource)
|
||||
pub fn as_audio(&self) -> Option<&AudioContent> {
|
||||
match self {
|
||||
ContentBlock::Audio(audio) => Some(audio),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Content {
|
||||
pub fn text<S: Into<String>>(text: S) -> Self {
|
||||
RawContent::text(text).no_annotation()
|
||||
}
|
||||
|
||||
pub fn image<S: Into<String>, T: Into<String>>(data: S, mime_type: T) -> Self {
|
||||
RawContent::image(data, mime_type).no_annotation()
|
||||
}
|
||||
|
||||
pub fn resource(resource: ResourceContents) -> Self {
|
||||
RawContent::resource(resource).no_annotation()
|
||||
}
|
||||
|
||||
pub fn embedded_text<S: Into<String>, T: Into<String>>(uri: S, content: T) -> Self {
|
||||
RawContent::embedded_text(uri, content).no_annotation()
|
||||
}
|
||||
|
||||
pub fn json<S: Serialize>(json: S) -> Result<Self, crate::ErrorData> {
|
||||
RawContent::json(json).map(|c| c.no_annotation())
|
||||
}
|
||||
|
||||
/// Create a resource link content
|
||||
pub fn resource_link(resource: super::resource::RawResource) -> Self {
|
||||
RawContent::resource_link(resource).no_annotation()
|
||||
}
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// JsonContent (unchanged)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct JsonContent<S: Serialize>(S);
|
||||
/// Types that can be converted into a list of contents
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// IntoContents
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Types that can be converted into a list of content blocks.
|
||||
pub trait IntoContents {
|
||||
fn into_contents(self) -> Vec<Content>;
|
||||
fn into_contents(self) -> Vec<ContentBlock>;
|
||||
}
|
||||
|
||||
impl IntoContents for Content {
|
||||
fn into_contents(self) -> Vec<Content> {
|
||||
impl IntoContents for ContentBlock {
|
||||
fn into_contents(self) -> Vec<ContentBlock> {
|
||||
vec![self]
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoContents for String {
|
||||
fn into_contents(self) -> Vec<Content> {
|
||||
vec![Content::text(self)]
|
||||
fn into_contents(self) -> Vec<ContentBlock> {
|
||||
vec![ContentBlock::text(self)]
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoContents for () {
|
||||
fn into_contents(self) -> Vec<Content> {
|
||||
fn into_contents(self) -> Vec<ContentBlock> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
|
@ -305,40 +376,32 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_image_content_serialization() {
|
||||
let image_content = RawImageContent {
|
||||
data: "base64data".to_string(),
|
||||
mime_type: "image/png".to_string(),
|
||||
meta: None,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&image_content).unwrap();
|
||||
println!("ImageContent JSON: {}", json);
|
||||
|
||||
// Verify it contains mimeType (camelCase) not mime_type (snake_case)
|
||||
let image = ImageContent::new("base64data", "image/png");
|
||||
let json = serde_json::to_string(&image).unwrap();
|
||||
assert!(json.contains("mimeType"));
|
||||
assert!(!json.contains("mime_type"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_audio_content_serialization() {
|
||||
let audio_content = RawAudioContent {
|
||||
data: "base64audiodata".to_string(),
|
||||
mime_type: "audio/wav".to_string(),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&audio_content).unwrap();
|
||||
println!("AudioContent JSON: {}", json);
|
||||
|
||||
// Verify it contains mimeType (camelCase) not mime_type (snake_case)
|
||||
let audio = AudioContent::new("base64audiodata", "audio/wav");
|
||||
let json = serde_json::to_string(&audio).unwrap();
|
||||
assert!(json.contains("mimeType"));
|
||||
assert!(!json.contains("mime_type"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resource_link_serialization() {
|
||||
use super::super::resource::RawResource;
|
||||
fn test_audio_content_has_meta() {
|
||||
let audio = AudioContent::new("data", "audio/wav").with_meta(Meta::default());
|
||||
let json = serde_json::to_value(&audio).unwrap();
|
||||
assert!(json.get("_meta").is_some());
|
||||
}
|
||||
|
||||
let resource_link = RawContent::ResourceLink(RawResource {
|
||||
#[test]
|
||||
fn test_resource_link_serialization() {
|
||||
use super::super::resource::Resource;
|
||||
|
||||
let resource_link = ContentBlock::ResourceLink(Resource {
|
||||
uri: "file:///test.txt".to_string(),
|
||||
name: "test.txt".to_string(),
|
||||
title: None,
|
||||
|
|
@ -347,12 +410,10 @@ mod tests {
|
|||
size: Some(100),
|
||||
icons: None,
|
||||
meta: None,
|
||||
annotations: None,
|
||||
});
|
||||
|
||||
let json = serde_json::to_string(&resource_link).unwrap();
|
||||
println!("ResourceLink JSON: {}", json);
|
||||
|
||||
// Verify it contains the correct type tag
|
||||
assert!(json.contains("\"type\":\"resource_link\""));
|
||||
assert!(json.contains("\"uri\":\"file:///test.txt\""));
|
||||
assert!(json.contains("\"name\":\"test.txt\""));
|
||||
|
|
@ -368,9 +429,9 @@ mod tests {
|
|||
"mimeType": "text/plain"
|
||||
}"#;
|
||||
|
||||
let content: RawContent = serde_json::from_str(json).unwrap();
|
||||
let content: ContentBlock = serde_json::from_str(json).unwrap();
|
||||
|
||||
if let RawContent::ResourceLink(resource) = content {
|
||||
if let ContentBlock::ResourceLink(resource) = content {
|
||||
assert_eq!(resource.uri, "file:///example.txt");
|
||||
assert_eq!(resource.name, "example.txt");
|
||||
assert_eq!(resource.description, Some("Example file".to_string()));
|
||||
|
|
@ -379,4 +440,15 @@ mod tests {
|
|||
panic!("Expected ResourceLink variant");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_content_block_text_with_annotations() {
|
||||
let block = ContentBlock::Text(
|
||||
TextContent::new("hello").with_annotations(Annotations::default().with_priority(0.8)),
|
||||
);
|
||||
let json = serde_json::to_value(&block).unwrap();
|
||||
assert_eq!(json["type"], "text");
|
||||
assert_eq!(json["text"], "hello");
|
||||
assert_eq!(json["annotations"]["priority"], 0.8_f32);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,8 +49,8 @@ const_string!(ArrayTypeConst = "array");
|
|||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")]
|
||||
pub enum PrimitiveSchema {
|
||||
#[non_exhaustive]
|
||||
pub enum PrimitiveSchemaDefinition {
|
||||
/// Enum property (explicit enum schema)
|
||||
Enum(EnumSchema),
|
||||
/// String property (with optional enum constraint)
|
||||
|
|
@ -63,6 +63,9 @@ pub enum PrimitiveSchema {
|
|||
Boolean(BooleanSchema),
|
||||
}
|
||||
|
||||
#[deprecated(since = "2.0.0", note = "Renamed to PrimitiveSchemaDefinition")]
|
||||
pub type PrimitiveSchema = PrimitiveSchemaDefinition;
|
||||
|
||||
// =============================================================================
|
||||
// STRING SCHEMA
|
||||
// =============================================================================
|
||||
|
|
@ -71,7 +74,7 @@ pub enum PrimitiveSchema {
|
|||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")]
|
||||
#[non_exhaustive]
|
||||
pub enum StringFormat {
|
||||
/// Email address format
|
||||
Email,
|
||||
|
|
@ -346,7 +349,7 @@ impl NumberSchema {
|
|||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
|
||||
#[non_exhaustive]
|
||||
pub struct IntegerSchema {
|
||||
/// Type discriminator
|
||||
#[serde(rename = "type")]
|
||||
|
|
@ -513,7 +516,7 @@ impl BooleanSchema {
|
|||
/// Represent single entry for titled item
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
|
||||
#[non_exhaustive]
|
||||
pub struct ConstTitle {
|
||||
#[serde(rename = "const")]
|
||||
pub const_: String,
|
||||
|
|
@ -534,7 +537,7 @@ impl ConstTitle {
|
|||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
|
||||
#[non_exhaustive]
|
||||
pub struct LegacyEnumSchema {
|
||||
#[serde(rename = "type")]
|
||||
pub type_: StringTypeConst,
|
||||
|
|
@ -546,6 +549,21 @@ pub struct LegacyEnumSchema {
|
|||
pub enum_: Vec<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enum_names: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub default: Option<String>,
|
||||
}
|
||||
|
||||
impl LegacyEnumSchema {
|
||||
pub fn new(enum_values: Vec<String>) -> Self {
|
||||
Self {
|
||||
type_: StringTypeConst,
|
||||
title: None,
|
||||
description: None,
|
||||
enum_: enum_values,
|
||||
enum_names: None,
|
||||
default: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Untitled single-select
|
||||
|
|
@ -601,7 +619,7 @@ impl TitledSingleSelectEnumSchema {
|
|||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")]
|
||||
#[non_exhaustive]
|
||||
pub enum SingleSelectEnumSchema {
|
||||
Untitled(UntitledSingleSelectEnumSchema),
|
||||
Titled(TitledSingleSelectEnumSchema),
|
||||
|
|
@ -610,7 +628,7 @@ pub enum SingleSelectEnumSchema {
|
|||
/// Items for untitled multi-select options
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
|
||||
#[non_exhaustive]
|
||||
pub struct UntitledItems {
|
||||
#[serde(rename = "type")]
|
||||
pub type_: StringTypeConst,
|
||||
|
|
@ -618,10 +636,19 @@ pub struct UntitledItems {
|
|||
pub enum_: Vec<String>,
|
||||
}
|
||||
|
||||
impl UntitledItems {
|
||||
pub fn new(enum_values: Vec<String>) -> Self {
|
||||
Self {
|
||||
type_: StringTypeConst,
|
||||
enum_: enum_values,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Items for titled multi-select options
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
|
||||
#[non_exhaustive]
|
||||
pub struct TitledItems {
|
||||
// MCP spec requires "anyOf" for multi-select enums (allows any combination)
|
||||
// Alias "oneOf" for compatibility with schemars
|
||||
|
|
@ -727,7 +754,7 @@ impl TitledMultiSelectEnumSchema {
|
|||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")]
|
||||
#[non_exhaustive]
|
||||
pub enum MultiSelectEnumSchema {
|
||||
Untitled(UntitledMultiSelectEnumSchema),
|
||||
Titled(TitledMultiSelectEnumSchema),
|
||||
|
|
@ -751,7 +778,7 @@ pub enum MultiSelectEnumSchema {
|
|||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")]
|
||||
#[non_exhaustive]
|
||||
pub enum EnumSchema {
|
||||
Single(SingleSelectEnumSchema),
|
||||
Multi(MultiSelectEnumSchema),
|
||||
|
|
@ -1090,7 +1117,7 @@ impl EnumSchema {
|
|||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
|
||||
#[non_exhaustive]
|
||||
pub struct ElicitationSchema {
|
||||
/// Always "object" for elicitation schemas
|
||||
#[serde(rename = "type")]
|
||||
|
|
@ -1101,7 +1128,7 @@ pub struct ElicitationSchema {
|
|||
pub title: Option<Cow<'static, str>>,
|
||||
|
||||
/// Property definitions (must be primitive types)
|
||||
pub properties: BTreeMap<String, PrimitiveSchema>,
|
||||
pub properties: BTreeMap<String, PrimitiveSchemaDefinition>,
|
||||
|
||||
/// List of required property names
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
|
@ -1114,7 +1141,7 @@ pub struct ElicitationSchema {
|
|||
|
||||
impl ElicitationSchema {
|
||||
/// Create a new elicitation schema with the given properties
|
||||
pub fn new(properties: BTreeMap<String, PrimitiveSchema>) -> Self {
|
||||
pub fn new(properties: BTreeMap<String, PrimitiveSchemaDefinition>) -> Self {
|
||||
Self {
|
||||
type_: ObjectTypeConst,
|
||||
title: None,
|
||||
|
|
@ -1237,7 +1264,7 @@ impl ElicitationSchema {
|
|||
#[derive(Debug, Default)]
|
||||
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
|
||||
pub struct ElicitationSchemaBuilder {
|
||||
pub properties: BTreeMap<String, PrimitiveSchema>,
|
||||
pub properties: BTreeMap<String, PrimitiveSchemaDefinition>,
|
||||
pub required: Vec<String>,
|
||||
pub title: Option<Cow<'static, str>>,
|
||||
pub description: Option<Cow<'static, str>>,
|
||||
|
|
@ -1250,13 +1277,17 @@ impl ElicitationSchemaBuilder {
|
|||
}
|
||||
|
||||
/// Add a property to the schema
|
||||
pub fn property(mut self, name: impl Into<String>, schema: PrimitiveSchema) -> Self {
|
||||
pub fn property(mut self, name: impl Into<String>, schema: PrimitiveSchemaDefinition) -> Self {
|
||||
self.properties.insert(name.into(), schema);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a required property to the schema
|
||||
pub fn required_property(mut self, name: impl Into<String>, schema: PrimitiveSchema) -> Self {
|
||||
pub fn required_property(
|
||||
mut self,
|
||||
name: impl Into<String>,
|
||||
schema: PrimitiveSchemaDefinition,
|
||||
) -> Self {
|
||||
let name_str = name.into();
|
||||
self.required.push(name_str.clone());
|
||||
self.properties.insert(name_str, schema);
|
||||
|
|
@ -1264,7 +1295,7 @@ impl ElicitationSchemaBuilder {
|
|||
}
|
||||
|
||||
// ===========================================================================
|
||||
// TYPED PROPERTY METHODS - Cleaner API without PrimitiveSchema wrapper
|
||||
// TYPED PROPERTY METHODS - Cleaner API without PrimitiveSchemaDefinition wrapper
|
||||
// ===========================================================================
|
||||
|
||||
/// Add a string property with custom builder (required)
|
||||
|
|
@ -1273,8 +1304,10 @@ impl ElicitationSchemaBuilder {
|
|||
name: impl Into<String>,
|
||||
f: impl FnOnce(StringSchema) -> StringSchema,
|
||||
) -> Self {
|
||||
self.properties
|
||||
.insert(name.into(), PrimitiveSchema::String(f(StringSchema::new())));
|
||||
self.properties.insert(
|
||||
name.into(),
|
||||
PrimitiveSchemaDefinition::String(f(StringSchema::new())),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -1286,8 +1319,10 @@ impl ElicitationSchemaBuilder {
|
|||
) -> Self {
|
||||
let name_str = name.into();
|
||||
self.required.push(name_str.clone());
|
||||
self.properties
|
||||
.insert(name_str, PrimitiveSchema::String(f(StringSchema::new())));
|
||||
self.properties.insert(
|
||||
name_str,
|
||||
PrimitiveSchemaDefinition::String(f(StringSchema::new())),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -1297,8 +1332,10 @@ impl ElicitationSchemaBuilder {
|
|||
name: impl Into<String>,
|
||||
f: impl FnOnce(NumberSchema) -> NumberSchema,
|
||||
) -> Self {
|
||||
self.properties
|
||||
.insert(name.into(), PrimitiveSchema::Number(f(NumberSchema::new())));
|
||||
self.properties.insert(
|
||||
name.into(),
|
||||
PrimitiveSchemaDefinition::Number(f(NumberSchema::new())),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -1310,8 +1347,10 @@ impl ElicitationSchemaBuilder {
|
|||
) -> Self {
|
||||
let name_str = name.into();
|
||||
self.required.push(name_str.clone());
|
||||
self.properties
|
||||
.insert(name_str, PrimitiveSchema::Number(f(NumberSchema::new())));
|
||||
self.properties.insert(
|
||||
name_str,
|
||||
PrimitiveSchemaDefinition::Number(f(NumberSchema::new())),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -1323,7 +1362,7 @@ impl ElicitationSchemaBuilder {
|
|||
) -> Self {
|
||||
self.properties.insert(
|
||||
name.into(),
|
||||
PrimitiveSchema::Integer(f(IntegerSchema::new())),
|
||||
PrimitiveSchemaDefinition::Integer(f(IntegerSchema::new())),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
|
@ -1336,8 +1375,10 @@ impl ElicitationSchemaBuilder {
|
|||
) -> Self {
|
||||
let name_str = name.into();
|
||||
self.required.push(name_str.clone());
|
||||
self.properties
|
||||
.insert(name_str, PrimitiveSchema::Integer(f(IntegerSchema::new())));
|
||||
self.properties.insert(
|
||||
name_str,
|
||||
PrimitiveSchemaDefinition::Integer(f(IntegerSchema::new())),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -1349,7 +1390,7 @@ impl ElicitationSchemaBuilder {
|
|||
) -> Self {
|
||||
self.properties.insert(
|
||||
name.into(),
|
||||
PrimitiveSchema::Boolean(f(BooleanSchema::new())),
|
||||
PrimitiveSchemaDefinition::Boolean(f(BooleanSchema::new())),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
|
@ -1362,8 +1403,10 @@ impl ElicitationSchemaBuilder {
|
|||
) -> Self {
|
||||
let name_str = name.into();
|
||||
self.required.push(name_str.clone());
|
||||
self.properties
|
||||
.insert(name_str, PrimitiveSchema::Boolean(f(BooleanSchema::new())));
|
||||
self.properties.insert(
|
||||
name_str,
|
||||
PrimitiveSchemaDefinition::Boolean(f(BooleanSchema::new())),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -1373,22 +1416,28 @@ impl ElicitationSchemaBuilder {
|
|||
|
||||
/// Add a required string property
|
||||
pub fn required_string(self, name: impl Into<String>) -> Self {
|
||||
self.required_property(name, PrimitiveSchema::String(StringSchema::new()))
|
||||
self.required_property(name, PrimitiveSchemaDefinition::String(StringSchema::new()))
|
||||
}
|
||||
|
||||
/// Add an optional string property
|
||||
pub fn optional_string(self, name: impl Into<String>) -> Self {
|
||||
self.property(name, PrimitiveSchema::String(StringSchema::new()))
|
||||
self.property(name, PrimitiveSchemaDefinition::String(StringSchema::new()))
|
||||
}
|
||||
|
||||
/// Add a required email property
|
||||
pub fn required_email(self, name: impl Into<String>) -> Self {
|
||||
self.required_property(name, PrimitiveSchema::String(StringSchema::email()))
|
||||
self.required_property(
|
||||
name,
|
||||
PrimitiveSchemaDefinition::String(StringSchema::email()),
|
||||
)
|
||||
}
|
||||
|
||||
/// Add an optional email property
|
||||
pub fn optional_email(self, name: impl Into<String>) -> Self {
|
||||
self.property(name, PrimitiveSchema::String(StringSchema::email()))
|
||||
self.property(
|
||||
name,
|
||||
PrimitiveSchemaDefinition::String(StringSchema::email()),
|
||||
)
|
||||
}
|
||||
|
||||
/// Add a required string property with custom builder
|
||||
|
|
@ -1397,7 +1446,10 @@ impl ElicitationSchemaBuilder {
|
|||
name: impl Into<String>,
|
||||
f: impl FnOnce(StringSchema) -> StringSchema,
|
||||
) -> Self {
|
||||
self.required_property(name, PrimitiveSchema::String(f(StringSchema::new())))
|
||||
self.required_property(
|
||||
name,
|
||||
PrimitiveSchemaDefinition::String(f(StringSchema::new())),
|
||||
)
|
||||
}
|
||||
|
||||
/// Add an optional string property with custom builder
|
||||
|
|
@ -1406,7 +1458,10 @@ impl ElicitationSchemaBuilder {
|
|||
name: impl Into<String>,
|
||||
f: impl FnOnce(StringSchema) -> StringSchema,
|
||||
) -> Self {
|
||||
self.property(name, PrimitiveSchema::String(f(StringSchema::new())))
|
||||
self.property(
|
||||
name,
|
||||
PrimitiveSchemaDefinition::String(f(StringSchema::new())),
|
||||
)
|
||||
}
|
||||
|
||||
// Convenience methods for numbers
|
||||
|
|
@ -1415,7 +1470,7 @@ impl ElicitationSchemaBuilder {
|
|||
pub fn required_number(self, name: impl Into<String>, min: f64, max: f64) -> Self {
|
||||
self.required_property(
|
||||
name,
|
||||
PrimitiveSchema::Number(NumberSchema::new().range(min, max)),
|
||||
PrimitiveSchemaDefinition::Number(NumberSchema::new().range(min, max)),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1423,7 +1478,7 @@ impl ElicitationSchemaBuilder {
|
|||
pub fn optional_number(self, name: impl Into<String>, min: f64, max: f64) -> Self {
|
||||
self.property(
|
||||
name,
|
||||
PrimitiveSchema::Number(NumberSchema::new().range(min, max)),
|
||||
PrimitiveSchemaDefinition::Number(NumberSchema::new().range(min, max)),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1433,7 +1488,10 @@ impl ElicitationSchemaBuilder {
|
|||
name: impl Into<String>,
|
||||
f: impl FnOnce(NumberSchema) -> NumberSchema,
|
||||
) -> Self {
|
||||
self.required_property(name, PrimitiveSchema::Number(f(NumberSchema::new())))
|
||||
self.required_property(
|
||||
name,
|
||||
PrimitiveSchemaDefinition::Number(f(NumberSchema::new())),
|
||||
)
|
||||
}
|
||||
|
||||
/// Add an optional number property with custom builder
|
||||
|
|
@ -1442,7 +1500,10 @@ impl ElicitationSchemaBuilder {
|
|||
name: impl Into<String>,
|
||||
f: impl FnOnce(NumberSchema) -> NumberSchema,
|
||||
) -> Self {
|
||||
self.property(name, PrimitiveSchema::Number(f(NumberSchema::new())))
|
||||
self.property(
|
||||
name,
|
||||
PrimitiveSchemaDefinition::Number(f(NumberSchema::new())),
|
||||
)
|
||||
}
|
||||
|
||||
// Convenience methods for integers
|
||||
|
|
@ -1451,7 +1512,7 @@ impl ElicitationSchemaBuilder {
|
|||
pub fn required_integer(self, name: impl Into<String>, min: i64, max: i64) -> Self {
|
||||
self.required_property(
|
||||
name,
|
||||
PrimitiveSchema::Integer(IntegerSchema::new().range(min, max)),
|
||||
PrimitiveSchemaDefinition::Integer(IntegerSchema::new().range(min, max)),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1459,7 +1520,7 @@ impl ElicitationSchemaBuilder {
|
|||
pub fn optional_integer(self, name: impl Into<String>, min: i64, max: i64) -> Self {
|
||||
self.property(
|
||||
name,
|
||||
PrimitiveSchema::Integer(IntegerSchema::new().range(min, max)),
|
||||
PrimitiveSchemaDefinition::Integer(IntegerSchema::new().range(min, max)),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1469,7 +1530,10 @@ impl ElicitationSchemaBuilder {
|
|||
name: impl Into<String>,
|
||||
f: impl FnOnce(IntegerSchema) -> IntegerSchema,
|
||||
) -> Self {
|
||||
self.required_property(name, PrimitiveSchema::Integer(f(IntegerSchema::new())))
|
||||
self.required_property(
|
||||
name,
|
||||
PrimitiveSchemaDefinition::Integer(f(IntegerSchema::new())),
|
||||
)
|
||||
}
|
||||
|
||||
/// Add an optional integer property with custom builder
|
||||
|
|
@ -1478,21 +1542,27 @@ impl ElicitationSchemaBuilder {
|
|||
name: impl Into<String>,
|
||||
f: impl FnOnce(IntegerSchema) -> IntegerSchema,
|
||||
) -> Self {
|
||||
self.property(name, PrimitiveSchema::Integer(f(IntegerSchema::new())))
|
||||
self.property(
|
||||
name,
|
||||
PrimitiveSchemaDefinition::Integer(f(IntegerSchema::new())),
|
||||
)
|
||||
}
|
||||
|
||||
// Convenience methods for booleans
|
||||
|
||||
/// Add a required boolean property
|
||||
pub fn required_bool(self, name: impl Into<String>) -> Self {
|
||||
self.required_property(name, PrimitiveSchema::Boolean(BooleanSchema::new()))
|
||||
self.required_property(
|
||||
name,
|
||||
PrimitiveSchemaDefinition::Boolean(BooleanSchema::new()),
|
||||
)
|
||||
}
|
||||
|
||||
/// Add an optional boolean property with default value
|
||||
pub fn optional_bool(self, name: impl Into<String>, default: bool) -> Self {
|
||||
self.property(
|
||||
name,
|
||||
PrimitiveSchema::Boolean(BooleanSchema::new().with_default(default)),
|
||||
PrimitiveSchemaDefinition::Boolean(BooleanSchema::new().with_default(default)),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1502,7 +1572,10 @@ impl ElicitationSchemaBuilder {
|
|||
name: impl Into<String>,
|
||||
f: impl FnOnce(BooleanSchema) -> BooleanSchema,
|
||||
) -> Self {
|
||||
self.required_property(name, PrimitiveSchema::Boolean(f(BooleanSchema::new())))
|
||||
self.required_property(
|
||||
name,
|
||||
PrimitiveSchemaDefinition::Boolean(f(BooleanSchema::new())),
|
||||
)
|
||||
}
|
||||
|
||||
/// Add an optional boolean property with custom builder
|
||||
|
|
@ -1511,19 +1584,22 @@ impl ElicitationSchemaBuilder {
|
|||
name: impl Into<String>,
|
||||
f: impl FnOnce(BooleanSchema) -> BooleanSchema,
|
||||
) -> Self {
|
||||
self.property(name, PrimitiveSchema::Boolean(f(BooleanSchema::new())))
|
||||
self.property(
|
||||
name,
|
||||
PrimitiveSchemaDefinition::Boolean(f(BooleanSchema::new())),
|
||||
)
|
||||
}
|
||||
|
||||
// Enum convenience methods
|
||||
|
||||
/// Add a required enum property using EnumSchema
|
||||
pub fn required_enum_schema(self, name: impl Into<String>, enum_schema: EnumSchema) -> Self {
|
||||
self.required_property(name, PrimitiveSchema::Enum(enum_schema))
|
||||
self.required_property(name, PrimitiveSchemaDefinition::Enum(enum_schema))
|
||||
}
|
||||
|
||||
/// Add an optional enum property using EnumSchema
|
||||
pub fn optional_enum_schema(self, name: impl Into<String>, enum_schema: EnumSchema) -> Self {
|
||||
self.property(name, PrimitiveSchema::Enum(enum_schema))
|
||||
self.property(name, PrimitiveSchemaDefinition::Enum(enum_schema))
|
||||
}
|
||||
|
||||
/// Add a required enum property using values. Creates an untitled single-select enum.
|
||||
|
|
@ -1534,12 +1610,13 @@ impl ElicitationSchemaBuilder {
|
|||
pub fn required_enum(self, name: impl Into<String>, values: Vec<String>) -> Self {
|
||||
self.required_property(
|
||||
name,
|
||||
PrimitiveSchema::Enum(EnumSchema::Legacy(LegacyEnumSchema {
|
||||
PrimitiveSchemaDefinition::Enum(EnumSchema::Legacy(LegacyEnumSchema {
|
||||
type_: StringTypeConst,
|
||||
title: None,
|
||||
description: None,
|
||||
enum_: values,
|
||||
enum_names: None,
|
||||
default: None,
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
|
@ -1552,12 +1629,13 @@ impl ElicitationSchemaBuilder {
|
|||
pub fn optional_enum(self, name: impl Into<String>, values: Vec<String>) -> Self {
|
||||
self.property(
|
||||
name,
|
||||
PrimitiveSchema::Enum(EnumSchema::Legacy(LegacyEnumSchema {
|
||||
PrimitiveSchemaDefinition::Enum(EnumSchema::Legacy(LegacyEnumSchema {
|
||||
type_: StringTypeConst,
|
||||
title: None,
|
||||
description: None,
|
||||
enum_: values,
|
||||
enum_names: None,
|
||||
default: None,
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
|
@ -1732,6 +1810,7 @@ mod tests {
|
|||
description: Some("A legacy enum schema".into()),
|
||||
enum_: vec!["A".to_string(), "B".to_string()],
|
||||
enum_names: Some(vec!["Option A".to_string(), "Option B".to_string()]),
|
||||
default: None,
|
||||
});
|
||||
let json = serde_json::to_value(&schema)?;
|
||||
|
||||
|
|
@ -1776,6 +1855,7 @@ mod tests {
|
|||
description: None,
|
||||
enum_: vec!["a".to_string(), "b".to_string()],
|
||||
enum_names: None,
|
||||
default: None,
|
||||
});
|
||||
let json = serde_json::to_value(&schema)?;
|
||||
assert!(!json.as_object().unwrap().contains_key("enumNames"));
|
||||
|
|
@ -1970,14 +2050,14 @@ mod tests {
|
|||
"type": "string",
|
||||
"enum": ["a", "b"]
|
||||
});
|
||||
let schema: PrimitiveSchema = serde_json::from_value(json).unwrap();
|
||||
assert!(matches!(schema, PrimitiveSchema::Enum(_)));
|
||||
let schema: PrimitiveSchemaDefinition = serde_json::from_value(json).unwrap();
|
||||
assert!(matches!(schema, PrimitiveSchemaDefinition::Enum(_)));
|
||||
// Test that string schemas deserialize as String variant
|
||||
let json = json!({
|
||||
"type": "string"
|
||||
});
|
||||
let schema: PrimitiveSchema = serde_json::from_value(json).unwrap();
|
||||
assert!(matches!(schema, PrimitiveSchema::String(_)));
|
||||
let schema: PrimitiveSchemaDefinition = serde_json::from_value(json).unwrap();
|
||||
assert!(matches!(schema, PrimitiveSchemaDefinition::String(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use serde_json::Value;
|
|||
|
||||
use super::{
|
||||
ClientNotification, ClientRequest, CustomNotification, CustomRequest, Extensions, JsonObject,
|
||||
JsonRpcMessage, NumberOrString, ProgressToken, ServerNotification, ServerRequest,
|
||||
JsonRpcMessage, NumberOrString, ProgressToken, ServerNotification, ServerRequest, TaskMetadata,
|
||||
};
|
||||
|
||||
pub trait GetMeta {
|
||||
|
|
@ -54,11 +54,11 @@ pub trait RequestParamsMeta {
|
|||
/// can include a `task` field to signal that the caller wants task-augmented execution.
|
||||
pub trait TaskAugmentedRequestParamsMeta: RequestParamsMeta {
|
||||
/// Get a reference to the task field
|
||||
fn task(&self) -> Option<&JsonObject>;
|
||||
fn task(&self) -> Option<&TaskMetadata>;
|
||||
/// Get a mutable reference to the task field
|
||||
fn task_mut(&mut self) -> &mut Option<JsonObject>;
|
||||
fn task_mut(&mut self) -> &mut Option<TaskMetadata>;
|
||||
/// Set the task field
|
||||
fn set_task(&mut self, task: JsonObject) {
|
||||
fn set_task(&mut self, task: TaskMetadata) {
|
||||
*self.task_mut() = Some(task);
|
||||
}
|
||||
}
|
||||
|
|
@ -152,9 +152,9 @@ variant_extension! {
|
|||
CallToolRequest
|
||||
ListToolsRequest
|
||||
CustomRequest
|
||||
GetTaskInfoRequest
|
||||
GetTaskRequest
|
||||
ListTasksRequest
|
||||
GetTaskResultRequest
|
||||
GetTaskPayloadRequest
|
||||
CancelTaskRequest
|
||||
}
|
||||
}
|
||||
|
|
@ -164,7 +164,7 @@ variant_extension! {
|
|||
PingRequest
|
||||
CreateMessageRequest
|
||||
ListRootsRequest
|
||||
CreateElicitationRequest
|
||||
ElicitRequest
|
||||
CustomRequest
|
||||
}
|
||||
}
|
||||
|
|
@ -175,6 +175,7 @@ variant_extension! {
|
|||
ProgressNotification
|
||||
InitializedNotification
|
||||
RootsListChangedNotification
|
||||
TaskStatusNotification
|
||||
CustomNotification
|
||||
}
|
||||
}
|
||||
|
|
@ -188,7 +189,8 @@ variant_extension! {
|
|||
ResourceListChangedNotification
|
||||
ToolListChangedNotification
|
||||
PromptListChangedNotification
|
||||
ElicitationCompletionNotification
|
||||
ElicitationCompleteNotification
|
||||
TaskStatusNotification
|
||||
CustomNotification
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,37 +1,31 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{
|
||||
AnnotateAble, Annotations, Icon, Meta, RawEmbeddedResource,
|
||||
content::{AudioContent, EmbeddedResource, ImageContent},
|
||||
Annotations, ContentBlock, Icon, Meta, Role,
|
||||
content::{AudioContent, EmbeddedResource, ImageContent, TextContent},
|
||||
resource::ResourceContents,
|
||||
};
|
||||
|
||||
/// A prompt that can be used to generate text from a model
|
||||
/// A prompt or prompt template that the server offers (spec `Prompt`).
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[non_exhaustive]
|
||||
pub struct Prompt {
|
||||
/// The name of the prompt
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
/// Optional description of what the prompt does
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
/// Optional arguments that can be passed to customize the prompt
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub arguments: Option<Vec<PromptArgument>>,
|
||||
/// Optional list of icons for the prompt
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub icons: Option<Vec<Icon>>,
|
||||
/// Optional additional metadata for this prompt
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<Meta>,
|
||||
}
|
||||
|
||||
impl Prompt {
|
||||
/// Create a new prompt with the given name, description and arguments
|
||||
pub fn new<N, D>(
|
||||
name: N,
|
||||
description: Option<D>,
|
||||
|
|
@ -51,7 +45,6 @@ impl Prompt {
|
|||
}
|
||||
}
|
||||
|
||||
/// Create a new prompt from raw fields (used by the macro)
|
||||
pub fn from_raw(
|
||||
name: impl Into<String>,
|
||||
description: Option<impl Into<String>>,
|
||||
|
|
@ -67,45 +60,37 @@ impl Prompt {
|
|||
}
|
||||
}
|
||||
|
||||
/// Set the human-readable title
|
||||
pub fn with_title(mut self, title: impl Into<String>) -> Self {
|
||||
self.title = Some(title.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the icons
|
||||
pub fn with_icons(mut self, icons: Vec<Icon>) -> Self {
|
||||
self.icons = Some(icons);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the metadata
|
||||
pub fn with_meta(mut self, meta: Meta) -> Self {
|
||||
self.meta = Some(meta);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a prompt argument that can be passed to customize the prompt
|
||||
/// Describes an argument that a prompt can accept (spec `PromptArgument`).
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[non_exhaustive]
|
||||
pub struct PromptArgument {
|
||||
/// The name of the argument
|
||||
pub name: String,
|
||||
/// A human-readable title for the argument
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
/// A description of what the argument is used for
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
/// Whether this argument is required
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub required: Option<bool>,
|
||||
}
|
||||
|
||||
impl PromptArgument {
|
||||
/// Create a new prompt argument
|
||||
pub fn new<N: Into<String>>(name: N) -> Self {
|
||||
PromptArgument {
|
||||
name: name.into(),
|
||||
|
|
@ -115,108 +100,51 @@ impl PromptArgument {
|
|||
}
|
||||
}
|
||||
|
||||
/// Set the title
|
||||
pub fn with_title<T: Into<String>>(mut self, title: T) -> Self {
|
||||
self.title = Some(title.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the description
|
||||
pub fn with_description<D: Into<String>>(mut self, description: D) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the required flag
|
||||
pub fn with_required(mut self, required: bool) -> Self {
|
||||
self.required = Some(required);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the role of a message sender in a prompt conversation
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")]
|
||||
pub enum PromptMessageRole {
|
||||
User,
|
||||
Assistant,
|
||||
}
|
||||
|
||||
/// Content types that can be included in prompt messages
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")]
|
||||
pub enum PromptMessageContent {
|
||||
/// Plain text content
|
||||
Text { text: String },
|
||||
/// Image content with base64-encoded data
|
||||
Image {
|
||||
#[serde(flatten)]
|
||||
image: ImageContent,
|
||||
},
|
||||
/// Audio content with base64-encoded data
|
||||
Audio {
|
||||
#[serde(flatten)]
|
||||
audio: AudioContent,
|
||||
},
|
||||
/// Embedded server-side resource
|
||||
Resource {
|
||||
#[serde(flatten)]
|
||||
resource: EmbeddedResource,
|
||||
},
|
||||
/// A link to a resource that can be fetched separately
|
||||
ResourceLink {
|
||||
#[serde(flatten)]
|
||||
link: super::resource::Resource,
|
||||
},
|
||||
}
|
||||
|
||||
impl PromptMessageContent {
|
||||
pub fn text(text: impl Into<String>) -> Self {
|
||||
Self::Text { text: text.into() }
|
||||
}
|
||||
|
||||
/// Create a resource link content
|
||||
pub fn resource_link(resource: super::resource::Resource) -> Self {
|
||||
Self::ResourceLink { link: resource }
|
||||
}
|
||||
}
|
||||
|
||||
/// A message in a prompt conversation
|
||||
/// A message returned as part of a prompt (spec `PromptMessage`).
|
||||
///
|
||||
/// Uses the unified `ContentBlock` for its content (text | image | audio | resource_link | resource).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[non_exhaustive]
|
||||
pub struct PromptMessage {
|
||||
/// The role of the message sender
|
||||
pub role: PromptMessageRole,
|
||||
/// The content of the message
|
||||
pub content: PromptMessageContent,
|
||||
pub role: Role,
|
||||
pub content: ContentBlock,
|
||||
}
|
||||
|
||||
impl PromptMessage {
|
||||
/// Create a new prompt message with the given role and content
|
||||
pub fn new(role: PromptMessageRole, content: PromptMessageContent) -> Self {
|
||||
pub fn new(role: Role, content: ContentBlock) -> Self {
|
||||
Self { role, content }
|
||||
}
|
||||
|
||||
/// Create a new text message with the given role and text content
|
||||
pub fn new_text<S: Into<String>>(role: PromptMessageRole, text: S) -> Self {
|
||||
pub fn new_text<S: Into<String>>(role: Role, text: S) -> Self {
|
||||
Self {
|
||||
role,
|
||||
content: PromptMessageContent::Text { text: text.into() },
|
||||
content: ContentBlock::text(text),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new image message. `meta` and `annotations` are optional.
|
||||
#[cfg(feature = "base64")]
|
||||
pub fn new_image(
|
||||
role: PromptMessageRole,
|
||||
role: Role,
|
||||
data: &[u8],
|
||||
mime_type: &str,
|
||||
meta: Option<crate::model::Meta>,
|
||||
meta: Option<Meta>,
|
||||
annotations: Option<Annotations>,
|
||||
) -> Self {
|
||||
use base64::{Engine, prelude::BASE64_STANDARD};
|
||||
|
|
@ -224,23 +152,21 @@ impl PromptMessage {
|
|||
let base64 = BASE64_STANDARD.encode(data);
|
||||
Self {
|
||||
role,
|
||||
content: PromptMessageContent::Image {
|
||||
image: crate::model::RawImageContent {
|
||||
data: base64,
|
||||
mime_type: mime_type.into(),
|
||||
meta,
|
||||
}
|
||||
.optional_annotate(annotations),
|
||||
},
|
||||
content: ContentBlock::Image(ImageContent {
|
||||
data: base64,
|
||||
mime_type: mime_type.into(),
|
||||
meta,
|
||||
annotations,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new audio message. `annotations` is optional.
|
||||
#[cfg(feature = "base64")]
|
||||
pub fn new_audio(
|
||||
role: PromptMessageRole,
|
||||
role: Role,
|
||||
data: &[u8],
|
||||
mime_type: &str,
|
||||
meta: Option<Meta>,
|
||||
annotations: Option<Annotations>,
|
||||
) -> Self {
|
||||
use base64::{Engine, prelude::BASE64_STANDARD};
|
||||
|
|
@ -248,24 +174,22 @@ impl PromptMessage {
|
|||
let base64 = BASE64_STANDARD.encode(data);
|
||||
Self {
|
||||
role,
|
||||
content: PromptMessageContent::Audio {
|
||||
audio: crate::model::RawAudioContent {
|
||||
data: base64,
|
||||
mime_type: mime_type.into(),
|
||||
}
|
||||
.optional_annotate(annotations),
|
||||
},
|
||||
content: ContentBlock::Audio(AudioContent {
|
||||
data: base64,
|
||||
mime_type: mime_type.into(),
|
||||
meta,
|
||||
annotations,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new resource message. `resource_meta`, `resource_content_meta`, and `annotations` are optional.
|
||||
pub fn new_resource(
|
||||
role: PromptMessageRole,
|
||||
role: Role,
|
||||
uri: String,
|
||||
mime_type: Option<String>,
|
||||
text: Option<String>,
|
||||
resource_meta: Option<crate::model::Meta>,
|
||||
resource_content_meta: Option<crate::model::Meta>,
|
||||
resource_meta: Option<Meta>,
|
||||
resource_content_meta: Option<Meta>,
|
||||
annotations: Option<Annotations>,
|
||||
) -> Self {
|
||||
let resource_contents = match text {
|
||||
|
|
@ -284,31 +208,29 @@ impl PromptMessage {
|
|||
};
|
||||
Self {
|
||||
role,
|
||||
content: PromptMessageContent::Resource {
|
||||
resource: RawEmbeddedResource {
|
||||
meta: resource_meta,
|
||||
resource: resource_contents,
|
||||
}
|
||||
.optional_annotate(annotations),
|
||||
},
|
||||
content: ContentBlock::Resource(EmbeddedResource {
|
||||
meta: resource_meta,
|
||||
resource: resource_contents,
|
||||
annotations,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Note: PromptMessage text content does not carry protocol-level _meta per current schema.
|
||||
/// This function exists for API symmetry but ignores the meta parameter.
|
||||
pub fn new_text_with_meta<S: Into<String>>(
|
||||
role: PromptMessageRole,
|
||||
text: S,
|
||||
_meta: Option<crate::model::Meta>,
|
||||
) -> Self {
|
||||
Self::new_text(role, text)
|
||||
}
|
||||
|
||||
/// Create a new resource link message
|
||||
pub fn new_resource_link(role: PromptMessageRole, resource: super::resource::Resource) -> Self {
|
||||
pub fn new_text_with_meta<S: Into<String>>(role: Role, text: S, meta: Option<Meta>) -> Self {
|
||||
Self {
|
||||
role,
|
||||
content: PromptMessageContent::ResourceLink { link: resource },
|
||||
content: ContentBlock::Text(TextContent {
|
||||
text: text.into(),
|
||||
meta,
|
||||
annotations: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_resource_link(role: Role, resource: super::resource::Resource) -> Self {
|
||||
Self {
|
||||
role,
|
||||
content: ContentBlock::ResourceLink(resource),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -321,35 +243,15 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_prompt_message_image_serialization() {
|
||||
let image_content = crate::model::RawImageContent {
|
||||
data: "base64data".to_string(),
|
||||
mime_type: "image/png".to_string(),
|
||||
meta: None,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&image_content).unwrap();
|
||||
println!("PromptMessage ImageContent JSON: {}", json);
|
||||
|
||||
// Verify it contains mimeType (camelCase) not mime_type (snake_case)
|
||||
let image = ImageContent::new("base64data", "image/png");
|
||||
let json = serde_json::to_string(&image).unwrap();
|
||||
assert!(json.contains("mimeType"));
|
||||
assert!(!json.contains("mime_type"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prompt_message_audio_serialization_and_deserialization() {
|
||||
// Audio is part of the spec's ContentBlock union for prompt messages
|
||||
// (text | image | audio | resource_link | resource). Ensure the Audio
|
||||
// variant serializes to the flat, spec-compliant shape
|
||||
// `{ "type": "audio", "data", "mimeType" }` and parses back.
|
||||
// See: https://modelcontextprotocol.io/specification/2025-06-18/server/prompts
|
||||
let content = PromptMessageContent::Audio {
|
||||
audio: crate::model::RawAudioContent {
|
||||
data: "YXVkaW8=".to_string(),
|
||||
mime_type: "audio/wav".to_string(),
|
||||
}
|
||||
.no_annotation(),
|
||||
};
|
||||
|
||||
let content = ContentBlock::Audio(AudioContent::new("YXVkaW8=", "audio/wav"));
|
||||
let value = serde_json::to_value(&content).unwrap();
|
||||
assert_eq!(value.get("type").and_then(|v| v.as_str()), Some("audio"));
|
||||
assert_eq!(value.get("data").and_then(|v| v.as_str()), Some("YXVkaW8="));
|
||||
|
|
@ -359,18 +261,15 @@ mod tests {
|
|||
"expected camelCase mimeType, got: {value:#?}"
|
||||
);
|
||||
|
||||
// Regression: a spec-valid audio content block must deserialize into
|
||||
// the Audio variant (previously failed with "unknown variant `audio`").
|
||||
let json = r#"{"type":"audio","data":"YXVkaW8=","mimeType":"audio/wav"}"#;
|
||||
let parsed: PromptMessageContent = serde_json::from_str(json).unwrap();
|
||||
let parsed: ContentBlock = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(parsed, content);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "base64")]
|
||||
fn test_prompt_message_new_audio_constructor() {
|
||||
let message =
|
||||
PromptMessage::new_audio(PromptMessageRole::User, b"hello", "audio/wav", None);
|
||||
let message = PromptMessage::new_audio(Role::User, b"hello", "audio/wav", None, None);
|
||||
let value = serde_json::to_value(&message).unwrap();
|
||||
let content = value.get("content").expect("content present");
|
||||
assert_eq!(content.get("type").and_then(|v| v.as_str()), Some("audio"));
|
||||
|
|
@ -378,7 +277,6 @@ mod tests {
|
|||
content.get("mimeType").and_then(|v| v.as_str()),
|
||||
Some("audio/wav")
|
||||
);
|
||||
// base64 of "hello"
|
||||
assert_eq!(
|
||||
content.get("data").and_then(|v| v.as_str()),
|
||||
Some("aGVsbG8=")
|
||||
|
|
@ -387,16 +285,12 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_prompt_message_resource_link_serialization() {
|
||||
use super::super::resource::RawResource;
|
||||
use super::super::resource::Resource;
|
||||
|
||||
let resource = RawResource::new("file:///test.txt", "test.txt");
|
||||
let message =
|
||||
PromptMessage::new_resource_link(PromptMessageRole::User, resource.no_annotation());
|
||||
let resource = Resource::new("file:///test.txt", "test.txt");
|
||||
let message = PromptMessage::new_resource_link(Role::User, resource);
|
||||
|
||||
let json = serde_json::to_string(&message).unwrap();
|
||||
println!("PromptMessage with ResourceLink JSON: {}", json);
|
||||
|
||||
// Verify it contains the correct type tag
|
||||
assert!(json.contains("\"type\":\"resource_link\""));
|
||||
assert!(json.contains("\"uri\":\"file:///test.txt\""));
|
||||
assert!(json.contains("\"name\":\"test.txt\""));
|
||||
|
|
@ -404,12 +298,8 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_prompt_message_resource_serialization_is_flat() {
|
||||
// Regression test: PromptMessageContent::Resource must serialize to
|
||||
// the spec-compliant flat shape `{ "type": "resource", "resource": { "uri", "mimeType", "text" } }`
|
||||
// and NOT the double-nested shape `{ "type": "resource", "resource": { "resource": {...} } }`.
|
||||
// See: https://modelcontextprotocol.io/specification/2025-06-18/server/prompts
|
||||
let message = PromptMessage::new_resource(
|
||||
PromptMessageRole::User,
|
||||
Role::User,
|
||||
"alc://packages/sc/narrative".to_string(),
|
||||
Some("text/markdown".to_string()),
|
||||
Some("# Hello".to_string()),
|
||||
|
|
@ -419,8 +309,6 @@ mod tests {
|
|||
);
|
||||
|
||||
let value: serde_json::Value = serde_json::to_value(&message).unwrap();
|
||||
|
||||
// Drill into content
|
||||
let content = value.get("content").expect("content present");
|
||||
assert_eq!(
|
||||
content.get("type").and_then(|v| v.as_str()),
|
||||
|
|
@ -431,7 +319,6 @@ mod tests {
|
|||
.get("resource")
|
||||
.expect("resource field present at content level");
|
||||
|
||||
// Spec-compliant: resource.uri / resource.mimeType / resource.text MUST be flat
|
||||
assert_eq!(
|
||||
resource.get("uri").and_then(|v| v.as_str()),
|
||||
Some("alc://packages/sc/narrative"),
|
||||
|
|
@ -446,7 +333,6 @@ mod tests {
|
|||
Some("# Hello")
|
||||
);
|
||||
|
||||
// Regression guard: content.resource MUST NOT contain a nested `resource` key.
|
||||
assert!(
|
||||
resource.get("resource").is_none(),
|
||||
"double-nested resource detected (regression): {resource:#?}"
|
||||
|
|
@ -463,13 +349,13 @@ mod tests {
|
|||
"mimeType": "text/plain"
|
||||
}"#;
|
||||
|
||||
let content: PromptMessageContent = serde_json::from_str(json).unwrap();
|
||||
let content: ContentBlock = serde_json::from_str(json).unwrap();
|
||||
|
||||
if let PromptMessageContent::ResourceLink { link } = content {
|
||||
assert_eq!(link.uri, "file:///example.txt");
|
||||
assert_eq!(link.name, "example.txt");
|
||||
assert_eq!(link.description, Some("Example file".to_string()));
|
||||
assert_eq!(link.mime_type, Some("text/plain".to_string()));
|
||||
if let ContentBlock::ResourceLink(resource) = content {
|
||||
assert_eq!(resource.uri, "file:///example.txt");
|
||||
assert_eq!(resource.name, "example.txt");
|
||||
assert_eq!(resource.description, Some("Example file".to_string()));
|
||||
assert_eq!(resource.mime_type, Some("text/plain".to_string()));
|
||||
} else {
|
||||
panic!("Expected ResourceLink variant");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,66 +1,173 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{Annotated, Icon, Meta};
|
||||
use super::{Annotations, Icon, Meta};
|
||||
|
||||
/// Represents a resource in the extension with metadata
|
||||
/// A known resource that the server is capable of reading (spec `Resource`).
|
||||
///
|
||||
/// Also used as the inner type of `ContentBlock::ResourceLink` (spec `ResourceLink extends Resource`).
|
||||
#[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 RawResource {
|
||||
/// URI representing the resource location (e.g., "file:///path/to/file" or "str:///content")
|
||||
#[non_exhaustive]
|
||||
pub struct Resource {
|
||||
/// The URI of this resource (e.g. `file:///path/to/file`).
|
||||
pub uri: String,
|
||||
/// Name of the resource
|
||||
/// The programmatic name of the resource.
|
||||
pub name: String,
|
||||
/// Human-readable title of the resource
|
||||
/// Optional human-readable display title.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
/// Optional description of the resource
|
||||
/// Optional description of what this resource represents.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
/// MIME type of the resource content ("text" or "blob")
|
||||
/// The MIME type of this resource, if known.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mime_type: Option<String>,
|
||||
|
||||
/// The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known.
|
||||
///
|
||||
/// This can be used by Hosts to display file sizes and estimate context window us
|
||||
/// The size of the raw resource content in bytes (before base64/tokenization), if known.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub size: Option<u32>,
|
||||
/// Optional list of icons for the resource
|
||||
pub size: Option<u64>,
|
||||
/// Optional set of icons the client may display for this resource.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub icons: Option<Vec<Icon>>,
|
||||
/// Optional additional metadata for this resource
|
||||
/// Optional protocol-level metadata for this resource.
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<Meta>,
|
||||
/// Optional annotations describing how the client should use this resource.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub annotations: Option<Annotations>,
|
||||
}
|
||||
|
||||
pub type Resource = Annotated<RawResource>;
|
||||
impl Resource {
|
||||
pub fn new(uri: impl Into<String>, name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
uri: uri.into(),
|
||||
name: name.into(),
|
||||
title: None,
|
||||
description: None,
|
||||
mime_type: None,
|
||||
size: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
annotations: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_title(mut self, title: impl Into<String>) -> Self {
|
||||
self.title = Some(title.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_description(mut self, description: impl Into<String>) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
|
||||
self.mime_type = Some(mime_type.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_size(mut self, size: u64) -> Self {
|
||||
self.size = Some(size);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_icons(mut self, icons: Vec<Icon>) -> Self {
|
||||
self.icons = Some(icons);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_meta(mut self, meta: Meta) -> Self {
|
||||
self.meta = Some(meta);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_annotations(mut self, annotations: Annotations) -> Self {
|
||||
self.annotations = Some(annotations);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A template description for resources available on the server (spec `ResourceTemplate`).
|
||||
#[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 RawResourceTemplate {
|
||||
#[non_exhaustive]
|
||||
pub struct ResourceTemplate {
|
||||
/// An RFC 6570 URI template for constructing resource URIs.
|
||||
pub uri_template: String,
|
||||
/// The programmatic name of the resource template.
|
||||
pub name: String,
|
||||
/// Optional human-readable display title.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
/// Optional description of what this template is for.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
/// The MIME type for resources matching this template, if uniform.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mime_type: Option<String>,
|
||||
/// Optional list of icons for the resource template
|
||||
/// Optional set of icons the client may display for this template.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub icons: Option<Vec<Icon>>,
|
||||
/// Optional protocol-level metadata for this resource template.
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<Meta>,
|
||||
/// Optional annotations describing how the client should use this template.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub annotations: Option<Annotations>,
|
||||
}
|
||||
|
||||
pub type ResourceTemplate = Annotated<RawResourceTemplate>;
|
||||
impl ResourceTemplate {
|
||||
pub fn new(uri_template: impl Into<String>, name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
uri_template: uri_template.into(),
|
||||
name: name.into(),
|
||||
title: None,
|
||||
description: None,
|
||||
mime_type: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
annotations: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_title(mut self, title: impl Into<String>) -> Self {
|
||||
self.title = Some(title.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_description(mut self, description: impl Into<String>) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
|
||||
self.mime_type = Some(mime_type.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_icons(mut self, icons: Vec<Icon>) -> Self {
|
||||
self.icons = Some(icons);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_meta(mut self, meta: Meta) -> Self {
|
||||
self.meta = Some(meta);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_annotations(mut self, annotations: Annotations) -> Self {
|
||||
self.annotations = Some(annotations);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// The contents of a specific resource or sub-resource.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")]
|
||||
#[non_exhaustive]
|
||||
pub enum ResourceContents {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
TextResourceContents {
|
||||
|
|
@ -83,7 +190,6 @@ pub enum ResourceContents {
|
|||
}
|
||||
|
||||
impl ResourceContents {
|
||||
/// Create text resource contents.
|
||||
pub fn text(text: impl Into<String>, uri: impl Into<String>) -> Self {
|
||||
Self::TextResourceContents {
|
||||
uri: uri.into(),
|
||||
|
|
@ -93,7 +199,6 @@ impl ResourceContents {
|
|||
}
|
||||
}
|
||||
|
||||
/// Create blob resource contents.
|
||||
pub fn blob(blob: impl Into<String>, uri: impl Into<String>) -> Self {
|
||||
Self::BlobResourceContents {
|
||||
uri: uri.into(),
|
||||
|
|
@ -103,7 +208,6 @@ impl ResourceContents {
|
|||
}
|
||||
}
|
||||
|
||||
/// Set the MIME type on this resource contents.
|
||||
pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
|
||||
match &mut self {
|
||||
Self::TextResourceContents { mime_type: mt, .. } => *mt = Some(mime_type.into()),
|
||||
|
|
@ -112,7 +216,6 @@ impl ResourceContents {
|
|||
self
|
||||
}
|
||||
|
||||
/// Set the metadata on this resource contents.
|
||||
pub fn with_meta(mut self, meta: Meta) -> Self {
|
||||
match &mut self {
|
||||
Self::TextResourceContents { meta: m, .. } => *m = Some(meta),
|
||||
|
|
@ -122,96 +225,6 @@ impl ResourceContents {
|
|||
}
|
||||
}
|
||||
|
||||
impl RawResource {
|
||||
/// Creates a new Resource from a URI with explicit mime type
|
||||
pub fn new(uri: impl Into<String>, name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
uri: uri.into(),
|
||||
name: name.into(),
|
||||
title: None,
|
||||
description: None,
|
||||
mime_type: None,
|
||||
size: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the human-readable title.
|
||||
pub fn with_title(mut self, title: impl Into<String>) -> Self {
|
||||
self.title = Some(title.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the description.
|
||||
pub fn with_description(mut self, description: impl Into<String>) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the MIME type.
|
||||
pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
|
||||
self.mime_type = Some(mime_type.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the size in bytes.
|
||||
pub fn with_size(mut self, size: u32) -> Self {
|
||||
self.size = Some(size);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the icons.
|
||||
pub fn with_icons(mut self, icons: Vec<Icon>) -> Self {
|
||||
self.icons = Some(icons);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the metadata.
|
||||
pub fn with_meta(mut self, meta: Meta) -> Self {
|
||||
self.meta = Some(meta);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RawResourceTemplate {
|
||||
/// Creates a new RawResourceTemplate with a URI template and name.
|
||||
pub fn new(uri_template: impl Into<String>, name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
uri_template: uri_template.into(),
|
||||
name: name.into(),
|
||||
title: None,
|
||||
description: None,
|
||||
mime_type: None,
|
||||
icons: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the human-readable title.
|
||||
pub fn with_title(mut self, title: impl Into<String>) -> Self {
|
||||
self.title = Some(title.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the description.
|
||||
pub fn with_description(mut self, description: impl Into<String>) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the MIME type.
|
||||
pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
|
||||
self.mime_type = Some(mime_type.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the icons.
|
||||
pub fn with_icons(mut self, icons: Vec<Icon>) -> Self {
|
||||
self.icons = Some(icons);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json;
|
||||
|
|
@ -221,21 +234,12 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_resource_serialization() {
|
||||
let resource = RawResource {
|
||||
uri: "file:///test.txt".to_string(),
|
||||
title: None,
|
||||
name: "test".to_string(),
|
||||
description: Some("Test resource".to_string()),
|
||||
mime_type: Some("text/plain".to_string()),
|
||||
size: Some(100),
|
||||
icons: None,
|
||||
meta: None,
|
||||
};
|
||||
let resource = Resource::new("file:///test.txt", "test")
|
||||
.with_description("Test resource")
|
||||
.with_mime_type("text/plain")
|
||||
.with_size(100);
|
||||
|
||||
let json = serde_json::to_string(&resource).unwrap();
|
||||
println!("Serialized JSON: {}", json);
|
||||
|
||||
// Verify it contains mimeType (camelCase) not mime_type (snake_case)
|
||||
assert!(json.contains("mimeType"));
|
||||
assert!(!json.contains("mime_type"));
|
||||
}
|
||||
|
|
@ -250,28 +254,22 @@ mod tests {
|
|||
};
|
||||
|
||||
let json = serde_json::to_string(&text_contents).unwrap();
|
||||
println!("ResourceContents JSON: {}", json);
|
||||
|
||||
// Verify it contains mimeType (camelCase) not mime_type (snake_case)
|
||||
assert!(json.contains("mimeType"));
|
||||
assert!(!json.contains("mime_type"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resource_template_with_icons() {
|
||||
let resource_template = RawResourceTemplate {
|
||||
uri_template: "file:///{path}".to_string(),
|
||||
name: "template".to_string(),
|
||||
title: Some("Test Template".to_string()),
|
||||
description: Some("A test resource template".to_string()),
|
||||
mime_type: Some("text/plain".to_string()),
|
||||
icons: Some(vec![Icon {
|
||||
let resource_template = ResourceTemplate::new("file:///{path}", "template")
|
||||
.with_title("Test Template")
|
||||
.with_description("A test resource template")
|
||||
.with_mime_type("text/plain")
|
||||
.with_icons(vec![Icon {
|
||||
src: "https://example.com/icon.png".to_string(),
|
||||
mime_type: Some("image/png".to_string()),
|
||||
sizes: Some(vec!["48x48".to_string()]),
|
||||
theme: Some(IconTheme::Light),
|
||||
}]),
|
||||
};
|
||||
}]);
|
||||
|
||||
let json = serde_json::to_value(&resource_template).unwrap();
|
||||
assert!(json["icons"].is_array());
|
||||
|
|
@ -282,16 +280,31 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_resource_template_without_icons() {
|
||||
let resource_template = RawResourceTemplate {
|
||||
uri_template: "file:///{path}".to_string(),
|
||||
name: "template".to_string(),
|
||||
title: None,
|
||||
description: None,
|
||||
mime_type: None,
|
||||
icons: None,
|
||||
};
|
||||
|
||||
let resource_template = ResourceTemplate::new("file:///{path}", "template");
|
||||
let json = serde_json::to_value(&resource_template).unwrap();
|
||||
assert!(json.get("icons").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resource_size_u64() {
|
||||
let resource = Resource::new("file:///big", "big").with_size(5_000_000_000);
|
||||
let json = serde_json::to_value(&resource).unwrap();
|
||||
assert_eq!(json["size"], 5_000_000_000_u64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resource_with_annotations() {
|
||||
let resource = Resource::new("file:///test.txt", "test")
|
||||
.with_annotations(Annotations::default().with_priority(0.9));
|
||||
let json = serde_json::to_value(&resource).unwrap();
|
||||
assert_eq!(json["annotations"]["priority"], 0.9_f32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resource_template_with_meta() {
|
||||
let resource_template =
|
||||
ResourceTemplate::new("file:///{path}", "template").with_meta(Meta::default());
|
||||
let json = serde_json::to_value(&resource_template).unwrap();
|
||||
assert!(json.get("_meta").is_some());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,54 @@ use serde_json::Value;
|
|||
|
||||
use super::Meta;
|
||||
|
||||
/// Metadata for augmenting a request with task execution (spec `TaskMetadata`).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[non_exhaustive]
|
||||
pub struct TaskMetadata {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ttl: Option<u64>,
|
||||
}
|
||||
|
||||
impl TaskMetadata {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn with_ttl(mut self, ttl: u64) -> Self {
|
||||
self.ttl = Some(ttl);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Metadata for associating messages with a task (spec `RelatedTaskMetadata`).
|
||||
///
|
||||
/// Carried in `_meta` under the key `"io.modelcontextprotocol/related-task"`.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[non_exhaustive]
|
||||
pub struct RelatedTaskMetadata {
|
||||
pub task_id: String,
|
||||
}
|
||||
|
||||
impl RelatedTaskMetadata {
|
||||
pub fn new(task_id: impl Into<String>) -> Self {
|
||||
Self {
|
||||
task_id: task_id.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The well-known `_meta` key for related-task metadata.
|
||||
pub const META_KEY: &str = "io.modelcontextprotocol/related-task";
|
||||
}
|
||||
|
||||
/// Canonical task lifecycle status as defined by SEP-1686.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")]
|
||||
#[non_exhaustive]
|
||||
pub enum TaskStatus {
|
||||
/// The receiver accepted the request and is currently working on it.
|
||||
#[default]
|
||||
|
|
@ -95,12 +138,20 @@ impl Task {
|
|||
#[non_exhaustive]
|
||||
pub struct CreateTaskResult {
|
||||
pub task: Task,
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<Meta>,
|
||||
}
|
||||
|
||||
impl CreateTaskResult {
|
||||
/// Create a new CreateTaskResult.
|
||||
pub fn new(task: Task) -> Self {
|
||||
Self { task }
|
||||
Self { task, meta: None }
|
||||
}
|
||||
|
||||
/// Sets the protocol-level metadata for this result.
|
||||
pub fn with_meta(mut self, meta: Meta) -> Self {
|
||||
self.meta = Some(meta);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -111,7 +162,7 @@ impl CreateTaskResult {
|
|||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
|
||||
#[non_exhaustive]
|
||||
pub struct GetTaskResult {
|
||||
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<Meta>,
|
||||
|
|
@ -119,6 +170,12 @@ pub struct GetTaskResult {
|
|||
pub task: Task,
|
||||
}
|
||||
|
||||
impl GetTaskResult {
|
||||
pub fn new(task: Task) -> Self {
|
||||
Self { meta: None, task }
|
||||
}
|
||||
}
|
||||
|
||||
/// Response to a `tasks/result` request.
|
||||
///
|
||||
/// Per spec, the result structure matches the original request type
|
||||
|
|
@ -162,7 +219,7 @@ impl<'de> serde::Deserialize<'de> for GetTaskPayloadResult {
|
|||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
|
||||
#[non_exhaustive]
|
||||
pub struct CancelTaskResult {
|
||||
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<Meta>,
|
||||
|
|
@ -170,26 +227,8 @@ pub struct CancelTaskResult {
|
|||
pub task: Task,
|
||||
}
|
||||
|
||||
/// Paginated list of tasks
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
|
||||
pub struct TaskList {
|
||||
pub tasks: Vec<Task>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub next_cursor: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub total: Option<u64>,
|
||||
}
|
||||
|
||||
impl TaskList {
|
||||
/// Create a new TaskList.
|
||||
pub fn new(tasks: Vec<Task>) -> Self {
|
||||
Self {
|
||||
tasks,
|
||||
next_cursor: None,
|
||||
total: None,
|
||||
}
|
||||
impl CancelTaskResult {
|
||||
pub fn new(task: Task) -> Self {
|
||||
Self { meta: None, task }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -400,8 +400,9 @@ impl<R: ServiceRole> RequestHandle<R> {
|
|||
async fn send_timeout_cancel_notification(&self, reason: &str) {
|
||||
let notification = CancelledNotification {
|
||||
params: CancelledNotificationParam {
|
||||
request_id: self.id.clone(),
|
||||
request_id: Some(self.id.clone()),
|
||||
reason: Some(reason.to_owned()),
|
||||
meta: None,
|
||||
},
|
||||
method: crate::model::CancelledNotificationMethod,
|
||||
extensions: Default::default(),
|
||||
|
|
@ -473,8 +474,9 @@ impl<R: ServiceRole> RequestHandle<R> {
|
|||
.await;
|
||||
let notification = CancelledNotification {
|
||||
params: CancelledNotificationParam {
|
||||
request_id: self.id,
|
||||
request_id: Some(self.id),
|
||||
reason,
|
||||
meta: None,
|
||||
},
|
||||
method: crate::model::CancelledNotificationMethod,
|
||||
extensions: Default::default(),
|
||||
|
|
@ -1084,11 +1086,13 @@ where
|
|||
};
|
||||
let _ = responder.send(response);
|
||||
if let Some(param) = cancellation_param {
|
||||
if let Some(responder) = local_responder_pool.remove(¶m.request_id) {
|
||||
tracing::info!(id = %param.request_id, reason = param.reason, "cancelled");
|
||||
let _response_result = responder.send(Err(ServiceError::Cancelled {
|
||||
reason: param.reason.clone(),
|
||||
}));
|
||||
if let Some(request_id) = ¶m.request_id {
|
||||
if let Some(responder) = local_responder_pool.remove(request_id) {
|
||||
tracing::info!(id = %request_id, reason = param.reason, "cancelled");
|
||||
let _response_result = responder.send(Err(ServiceError::Cancelled {
|
||||
reason: param.reason.clone(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1201,9 +1205,11 @@ where
|
|||
// catch cancelled notification
|
||||
let mut notification = match notification.try_into() {
|
||||
Ok::<CancelledNotification, _>(cancelled) => {
|
||||
if let Some(ct) = local_ct_pool.remove(&cancelled.params.request_id) {
|
||||
tracing::info!(id = %cancelled.params.request_id, reason = cancelled.params.reason, "cancelled");
|
||||
ct.cancel();
|
||||
if let Some(request_id) = &cancelled.params.request_id {
|
||||
if let Some(ct) = local_ct_pool.remove(request_id) {
|
||||
tracing::info!(id = %request_id, reason = cancelled.params.reason, "cancelled");
|
||||
ct.cancel();
|
||||
}
|
||||
}
|
||||
cancelled.into()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ use url::Url;
|
|||
use super::*;
|
||||
#[cfg(feature = "elicitation")]
|
||||
use crate::model::{
|
||||
CreateElicitationRequest, CreateElicitationRequestParams, CreateElicitationResult,
|
||||
ElicitationAction, ElicitationCompletionNotification, ElicitationResponseNotificationParam,
|
||||
ElicitRequest, ElicitRequestParams, ElicitResult, ElicitationAction,
|
||||
ElicitationCompleteNotification, ElicitationResponseNotificationParam,
|
||||
};
|
||||
use crate::{
|
||||
model::{
|
||||
|
|
@ -464,11 +464,11 @@ impl Peer<RoleServer> {
|
|||
peer_req list_roots ListRootsRequest() => ListRootsResult
|
||||
);
|
||||
#[cfg(feature = "elicitation")]
|
||||
method!(peer_req create_elicitation CreateElicitationRequest(CreateElicitationRequestParams) => CreateElicitationResult);
|
||||
method!(peer_req create_elicitation ElicitRequest(ElicitRequestParams) => ElicitResult);
|
||||
#[cfg(feature = "elicitation")]
|
||||
method!(peer_req_with_timeout create_elicitation_with_timeout CreateElicitationRequest(CreateElicitationRequestParams) => CreateElicitationResult);
|
||||
method!(peer_req_with_timeout create_elicitation_with_timeout ElicitRequest(ElicitRequestParams) => ElicitResult);
|
||||
#[cfg(feature = "elicitation")]
|
||||
method!(peer_not notify_url_elicitation_completed ElicitationCompletionNotification(ElicitationResponseNotificationParam));
|
||||
method!(peer_not notify_url_elicitation_completed ElicitationCompleteNotification(ElicitationResponseNotificationParam));
|
||||
|
||||
method!(peer_not notify_cancelled CancelledNotification(CancelledNotificationParam));
|
||||
method!(peer_not notify_progress ProgressNotification(ProgressNotificationParam));
|
||||
|
|
@ -787,7 +787,7 @@ impl Peer<RoleServer> {
|
|||
|
||||
let response = self
|
||||
.create_elicitation_with_timeout(
|
||||
CreateElicitationRequestParams::FormElicitationParams {
|
||||
ElicitRequestParams::FormElicitationParams {
|
||||
meta: None,
|
||||
message: message.into(),
|
||||
requested_schema: schema,
|
||||
|
|
@ -920,7 +920,7 @@ impl Peer<RoleServer> {
|
|||
|
||||
let action = self
|
||||
.create_elicitation_with_timeout(
|
||||
CreateElicitationRequestParams::UrlElicitationParams {
|
||||
ElicitRequestParams::UrlElicitationParams {
|
||||
meta: None,
|
||||
message: message.into(),
|
||||
url: url.into().to_string(),
|
||||
|
|
|
|||
|
|
@ -429,9 +429,10 @@ impl LocalSessionWorker {
|
|||
notification: &JsonRpcNotification<ClientNotification>,
|
||||
) {
|
||||
if let ClientNotification::CancelledNotification(n) = ¬ification.notification {
|
||||
let request_id = n.params.request_id.clone();
|
||||
let resource = ResourceKey::McpRequestId(request_id);
|
||||
self.unregister_resource(&resource);
|
||||
if let Some(request_id) = n.params.request_id.clone() {
|
||||
let resource = ResourceKey::McpRequestId(request_id);
|
||||
self.unregister_resource(&resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn evict_expired_channels(&mut self) {
|
||||
|
|
@ -496,13 +497,17 @@ impl LocalSessionWorker {
|
|||
}),
|
||||
..
|
||||
}) => {
|
||||
if let Some(id) = self
|
||||
.resource_router
|
||||
.get(&ResourceKey::McpRequestId(request_id.clone()))
|
||||
{
|
||||
OutboundChannel::RequestWise {
|
||||
id: *id,
|
||||
close: false,
|
||||
if let Some(req_id) = request_id {
|
||||
if let Some(id) = self
|
||||
.resource_router
|
||||
.get(&ResourceKey::McpRequestId(req_id.clone()))
|
||||
{
|
||||
OutboundChannel::RequestWise {
|
||||
id: *id,
|
||||
close: false,
|
||||
}
|
||||
} else {
|
||||
OutboundChannel::Common
|
||||
}
|
||||
} else {
|
||||
OutboundChannel::Common
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue