use crate::error::Result;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use uuid::Uuid;
#[async_trait]
pub trait ToolHandler: Send + Sync {
async fn call(&self, arguments: HashMap<String, Value>) -> Result<ToolResult>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tool {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(rename = "inputSchema")]
pub input_schema: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub server_id: Option<Uuid>,
#[serde(skip_serializing_if = "Option::is_none")]
pub server_name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolInput {
pub name: String,
#[serde(default)]
pub arguments: HashMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolResult {
pub content: Vec<ToolResultContent>,
#[serde(skip_serializing_if = "Option::is_none", rename = "isError")]
pub is_error: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ToolResultContent {
Text {
text: String,
},
Image {
data: String,
#[serde(rename = "mimeType")]
mime_type: String,
},
Resource {
uri: String,
#[serde(skip_serializing_if = "Option::is_none", rename = "mimeType")]
mime_type: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCapability {
#[serde(default)]
pub streaming: bool,
#[serde(default)]
pub cancellable: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub estimated_duration_ms: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceTemplate {
#[serde(rename = "uriTemplate")]
pub uri_template: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", rename = "mimeType")]
pub mime_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Prompt {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<Vec<PromptArgument>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PromptArgument {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default)]
pub required: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetPromptRequest {
pub name: String,
#[serde(default)]
pub arguments: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetPromptResult {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub messages: Vec<PromptMessage>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PromptMessage {
pub role: String,
pub content: ToolResultContent,
}
impl Tool {
pub fn simple(name: impl Into<String>, description: impl Into<String>) -> Self {
Self {
name: name.into(),
description: Some(description.into()),
input_schema: serde_json::json!({
"type": "object",
"properties": {},
"required": []
}),
server_id: None,
server_name: None,
}
}
pub fn with_schema(
name: impl Into<String>,
description: impl Into<String>,
schema: Value,
) -> Self {
Self {
name: name.into(),
description: Some(description.into()),
input_schema: schema,
server_id: None,
server_name: None,
}
}
}
impl ToolResult {
pub fn text(text: impl Into<String>) -> Self {
Self {
content: vec![ToolResultContent::Text { text: text.into() }],
is_error: None,
}
}
pub fn error(message: impl Into<String>) -> Self {
Self {
content: vec![ToolResultContent::Text {
text: message.into(),
}],
is_error: Some(true),
}
}
pub fn with_content(content: Vec<ToolResultContent>) -> Self {
Self {
content,
is_error: None,
}
}
}
impl ToolResultContent {
pub fn text(text: impl Into<String>) -> Self {
Self::Text { text: text.into() }
}
pub fn image(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
Self::Image {
data: data.into(),
mime_type: mime_type.into(),
}
}
pub fn resource(uri: impl Into<String>) -> Self {
Self::Resource {
uri: uri.into(),
mime_type: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_tool() {
let tool = Tool::simple("test_tool", "A test tool");
assert_eq!(tool.name, "test_tool");
assert!(tool.description.is_some());
}
#[test]
fn test_tool_result() {
let result = ToolResult::text("Success");
assert_eq!(result.content.len(), 1);
assert!(result.is_error.is_none());
}
#[test]
fn test_error_result() {
let result = ToolResult::error("Failed");
assert_eq!(result.is_error, Some(true));
}
#[test]
fn test_tool_serialization() {
let tool = Tool::simple("test", "description");
let json = serde_json::to_string(&tool).unwrap();
assert!(json.contains("\"name\":\"test\""));
assert!(json.contains("inputSchema"));
}
}