use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use super::{content::ContentBlock, core::Cursor};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ToolAnnotations {
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub audience: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "destructiveHint")]
pub destructive_hint: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "idempotentHint")]
pub idempotent_hint: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "openWorldHint")]
pub open_world_hint: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "readOnlyHint")]
pub read_only_hint: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "taskHint")]
pub task_hint: Option<TaskHint>,
#[serde(flatten)]
pub custom: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum TaskHint {
Never,
Optional,
Always,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
#[serde(rename_all = "lowercase")]
pub enum TaskSupportMode {
#[default]
Forbidden,
Optional,
Required,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ToolExecution {
#[serde(rename = "taskSupport", skip_serializing_if = "Option::is_none")]
pub task_support: Option<TaskSupportMode>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tool {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(rename = "inputSchema")]
pub input_schema: ToolInputSchema,
#[serde(rename = "outputSchema", skip_serializing_if = "Option::is_none")]
pub output_schema: Option<ToolOutputSchema>,
#[serde(skip_serializing_if = "Option::is_none")]
pub execution: Option<ToolExecution>,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<ToolAnnotations>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icons: Option<Vec<super::core::Icon>>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
impl Default for Tool {
fn default() -> Self {
Self {
name: "unnamed_tool".to_string(), title: None,
description: None,
input_schema: ToolInputSchema::default(),
output_schema: None,
execution: None,
annotations: None,
icons: None,
meta: None,
}
}
}
impl Tool {
pub fn new(name: impl Into<String>) -> Self {
let name = name.into();
assert!(!name.trim().is_empty(), "Tool name cannot be empty");
Self {
name,
title: None,
description: None,
input_schema: ToolInputSchema::default(),
output_schema: None,
execution: None,
annotations: None,
icons: None,
meta: None,
}
}
pub fn with_description(name: impl Into<String>, description: impl Into<String>) -> Self {
let name = name.into();
assert!(!name.trim().is_empty(), "Tool name cannot be empty");
Self {
name,
title: None,
description: Some(description.into()),
input_schema: ToolInputSchema::default(),
output_schema: None,
execution: None,
annotations: None,
icons: None,
meta: None,
}
}
pub fn with_execution(mut self, execution: ToolExecution) -> Self {
self.execution = Some(execution);
self
}
pub fn with_input_schema(mut self, schema: ToolInputSchema) -> Self {
self.input_schema = schema;
self
}
pub fn with_output_schema(mut self, schema: ToolOutputSchema) -> Self {
self.output_schema = Some(schema);
self
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn with_annotations(mut self, annotations: ToolAnnotations) -> Self {
self.annotations = Some(annotations);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolInputSchema {
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub schema_type: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub properties: Option<HashMap<String, serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<Vec<String>>,
#[serde(
rename = "additionalProperties",
skip_serializing_if = "Option::is_none"
)]
pub additional_properties: Option<serde_json::Value>,
#[serde(flatten, default, skip_serializing_if = "HashMap::is_empty")]
pub extra_keywords: HashMap<String, serde_json::Value>,
}
impl Default for ToolInputSchema {
fn default() -> Self {
Self {
schema_type: Some(serde_json::Value::String("object".to_string())),
properties: None,
required: None,
additional_properties: None,
extra_keywords: HashMap::new(),
}
}
}
impl ToolInputSchema {
pub fn empty() -> Self {
Self::default()
}
pub fn with_properties(properties: HashMap<String, serde_json::Value>) -> Self {
Self {
schema_type: Some(serde_json::Value::String("object".to_string())),
properties: Some(properties),
required: None,
additional_properties: None,
extra_keywords: HashMap::new(),
}
}
pub fn with_required_properties(
properties: HashMap<String, serde_json::Value>,
required: Vec<String>,
) -> Self {
Self {
schema_type: Some(serde_json::Value::String("object".to_string())),
properties: Some(properties),
required: Some(required),
additional_properties: Some(serde_json::Value::Bool(false)),
extra_keywords: HashMap::new(),
}
}
pub fn add_property(mut self, name: String, property: serde_json::Value) -> Self {
self.properties
.get_or_insert_with(HashMap::new)
.insert(name, property);
self
}
pub fn require_property(mut self, name: String) -> Self {
let required = self.required.get_or_insert_with(Vec::new);
if !required.contains(&name) {
required.push(name);
}
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolOutputSchema {
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub schema_type: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub properties: Option<HashMap<String, serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<Vec<String>>,
#[serde(
rename = "additionalProperties",
skip_serializing_if = "Option::is_none"
)]
pub additional_properties: Option<serde_json::Value>,
#[serde(flatten, default, skip_serializing_if = "HashMap::is_empty")]
pub extra_keywords: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ListToolsRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub cursor: Option<Cursor>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub _meta: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListToolsResult {
pub tools: Vec<Tool>,
#[serde(rename = "nextCursor", skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<Cursor>,
#[serde(skip_serializing_if = "Option::is_none")]
pub _meta: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CallToolRequest {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<HashMap<String, serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub task: Option<crate::types::tasks::TaskMetadata>,
#[serde(skip_serializing_if = "Option::is_none")]
pub _meta: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CallToolResult {
pub content: Vec<ContentBlock>,
#[serde(rename = "isError", skip_serializing_if = "Option::is_none")]
pub is_error: Option<bool>,
#[serde(rename = "structuredContent", skip_serializing_if = "Option::is_none")]
pub structured_content: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub _meta: Option<serde_json::Value>,
#[serde(rename = "taskId", skip_serializing_if = "Option::is_none")]
pub task_id: Option<String>,
}
impl CallToolResult {
pub fn all_text(&self) -> String {
self.content
.iter()
.filter_map(|block| match block {
ContentBlock::Text(text) => Some(text.text.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n")
}
pub fn first_text(&self) -> Option<&str> {
self.content.first().and_then(|block| match block {
ContentBlock::Text(text) => Some(text.text.as_str()),
_ => None,
})
}
pub fn has_error(&self) -> bool {
self.is_error.unwrap_or(false)
}
pub fn to_display_string(&self) -> String {
let mut parts = Vec::new();
if self.has_error() {
parts.push("ERROR:".to_string());
}
for (i, block) in self.content.iter().enumerate() {
match block {
ContentBlock::Text(text) => {
parts.push(text.text.clone());
}
ContentBlock::Image(img) => {
parts.push(format!(
"[Image: {} bytes, type: {}]",
img.data.len(),
img.mime_type
));
}
ContentBlock::Audio(audio) => {
parts.push(format!(
"[Audio: {} bytes, type: {}]",
audio.data.len(),
audio.mime_type
));
}
ContentBlock::ResourceLink(link) => {
let desc = link.description.as_deref().unwrap_or("");
let mime = link
.mime_type
.as_deref()
.map(|m| format!(" [{}]", m))
.unwrap_or_default();
parts.push(format!(
"[Resource: {}{}{}{}]",
link.name,
mime,
if !desc.is_empty() { ": " } else { "" },
desc
));
}
ContentBlock::Resource(_resource) => {
parts.push(format!("[Embedded Resource #{}]", i + 1));
}
ContentBlock::ToolUse(tool_use) => {
parts.push(format!(
"[Tool Use: {} (id: {})]",
tool_use.name, tool_use.id
));
}
ContentBlock::ToolResult(tool_result) => {
parts.push(format!(
"[Tool Result for: {}{}]",
tool_result.tool_use_id,
if tool_result.is_error.unwrap_or(false) {
" (ERROR)"
} else {
""
}
));
}
}
}
if self.structured_content.is_some() {
parts.push("[Includes structured output]".to_string());
}
parts.join("\n")
}
}
impl From<turbomcp_core::types::core::Annotations> for super::Annotations {
fn from(core_ann: turbomcp_core::types::core::Annotations) -> Self {
let custom: std::collections::HashMap<String, serde_json::Value> =
core_ann.custom.into_iter().collect();
Self {
audience: core_ann.audience,
priority: core_ann.priority,
last_modified: core_ann.last_modified,
custom,
}
}
}
impl From<turbomcp_core::types::content::Content> for super::ContentBlock {
fn from(content: turbomcp_core::types::content::Content) -> Self {
use turbomcp_core::types::content::Content as CoreContent;
match content {
CoreContent::Text { text, annotations } => {
super::ContentBlock::Text(super::TextContent {
text,
annotations: annotations.map(Into::into),
meta: None,
})
}
CoreContent::Image {
data,
mime_type,
annotations,
} => super::ContentBlock::Image(super::ImageContent {
data: data.into(),
mime_type: mime_type.to_string().into(),
annotations: annotations.map(Into::into),
meta: None,
}),
CoreContent::Audio {
data,
mime_type,
annotations,
} => super::ContentBlock::Audio(super::AudioContent {
data: data.into(),
mime_type: mime_type.to_string().into(),
annotations: annotations.map(Into::into),
meta: None,
}),
CoreContent::Resource {
resource,
annotations,
} => {
let protocol_resource = if let Some(text) = resource.text {
super::ResourceContent::Text(super::TextResourceContents {
uri: resource.uri.to_string().into(),
mime_type: resource.mime_type.map(|mime| mime.to_string().into()),
text,
meta: None,
})
} else if let Some(blob) = resource.blob {
super::ResourceContent::Blob(super::BlobResourceContents {
uri: resource.uri.to_string().into(),
mime_type: resource.mime_type.map(|mime| mime.to_string().into()),
blob: blob.into(),
meta: None,
})
} else {
#[cfg(feature = "std")]
eprintln!(
"[turbomcp-protocol] WARNING: Resource '{}' has neither text nor blob content",
resource.uri
);
super::ResourceContent::Text(super::TextResourceContents {
uri: resource.uri.to_string().into(),
mime_type: resource.mime_type.map(|mime| mime.to_string().into()),
text: String::new(),
meta: None,
})
};
super::ContentBlock::Resource(super::EmbeddedResource {
resource: protocol_resource,
annotations: annotations.map(Into::into),
meta: None,
})
}
}
}
}
impl From<turbomcp_core::types::tools::CallToolResult> for CallToolResult {
fn from(core_result: turbomcp_core::types::tools::CallToolResult) -> Self {
Self {
content: core_result.content.into_iter().map(Into::into).collect(),
is_error: core_result.is_error,
structured_content: None,
_meta: core_result._meta,
task_id: None,
}
}
}
#[cfg(test)]
mod conversion_tests {
use super::*;
use turbomcp_core::types::content::Content as CoreContent;
use turbomcp_core::types::tools::CallToolResult as CoreCallToolResult;
#[test]
fn test_core_content_to_protocol_text() {
let core = CoreContent::text("hello world");
let protocol: ContentBlock = core.into();
match protocol {
ContentBlock::Text(text) => {
assert_eq!(text.text, "hello world");
assert!(text.annotations.is_none());
}
_ => panic!("Expected Text variant"),
}
}
#[test]
fn test_core_content_to_protocol_image() {
let core = CoreContent::image("base64data", "image/png");
let protocol: ContentBlock = core.into();
match protocol {
ContentBlock::Image(img) => {
assert_eq!(img.data, "base64data");
assert_eq!(img.mime_type, "image/png");
}
_ => panic!("Expected Image variant"),
}
}
#[test]
fn test_core_call_tool_result_to_protocol() {
let core = CoreCallToolResult::text("success");
let protocol: CallToolResult = core.into();
assert_eq!(protocol.content.len(), 1);
assert!(protocol.is_error.is_none());
assert!(protocol.structured_content.is_none());
assert!(protocol.task_id.is_none());
match &protocol.content[0] {
ContentBlock::Text(text) => assert_eq!(text.text, "success"),
_ => panic!("Expected Text content"),
}
}
#[test]
fn test_core_call_tool_result_error_preserved() {
let core = CoreCallToolResult::error("something failed");
let protocol: CallToolResult = core.into();
assert_eq!(protocol.is_error, Some(true));
match &protocol.content[0] {
ContentBlock::Text(text) => assert_eq!(text.text, "something failed"),
_ => panic!("Expected Text content"),
}
}
#[test]
fn test_annotations_conversion() {
use crate::types::Annotations;
use turbomcp_core::types::core::Annotations as CoreAnnotations;
let core = CoreAnnotations {
audience: Some(vec!["user".to_string(), "assistant".to_string()]),
priority: Some(0.75),
last_modified: Some("2025-01-13T12:00:00Z".to_string()),
custom: Default::default(),
};
let protocol: Annotations = core.into();
assert_eq!(
protocol.audience,
Some(vec!["user".to_string(), "assistant".to_string()])
);
assert_eq!(protocol.priority, Some(0.75));
assert_eq!(
protocol.last_modified,
Some("2025-01-13T12:00:00Z".to_string())
);
assert!(protocol.custom.is_empty());
}
}