1use base64::Engine as _;
4use serde_json::Value;
5
6#[derive(Debug, Clone)]
8pub struct ToolInput {
9 pub value: Value,
11}
12
13#[derive(Debug, Clone)]
15pub struct ToolOutput {
16 pub blocks: Vec<ToolOutputBlock>,
18 pub is_error: bool,
20}
21
22impl ToolOutput {
23 pub fn text(s: impl Into<String>) -> Self {
25 Self { blocks: vec![ToolOutputBlock::Text { text: s.into() }], is_error: false }
26 }
27
28 pub fn error(s: impl Into<String>) -> Self {
31 Self { blocks: vec![ToolOutputBlock::Text { text: s.into() }], is_error: true }
32 }
33
34 pub(crate) fn to_mcp_content(&self) -> Vec<Value> {
36 self.blocks
37 .iter()
38 .map(|b| match b {
39 ToolOutputBlock::Text { text } => {
40 serde_json::json!({ "type": "text", "text": text })
41 }
42 ToolOutputBlock::Image { mime_type, data } => serde_json::json!({
45 "type": "image",
46 "data": base64::engine::general_purpose::STANDARD.encode(data),
47 "mimeType": mime_type,
48 }),
49 })
50 .collect()
51 }
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum ToolOutputBlock {
58 Text { text: String },
60 Image { mime_type: String, data: Vec<u8> },
63}
64
65#[async_trait::async_trait]
69pub trait Tool: Send + Sync {
70 fn name(&self) -> &'static str;
76
77 fn description(&self) -> &'static str;
79
80 fn input_schema(&self) -> Value;
82
83 async fn call(&self, input: ToolInput) -> ToolOutput;
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 #[test]
95 fn an_image_block_crosses_as_an_mcp_image_content_block() {
96 let output = ToolOutput {
97 blocks: vec![ToolOutputBlock::Image {
98 mime_type: "image/png".to_owned(),
99 data: vec![0x00, 0xff, 0x10],
100 }],
101 is_error: false,
102 };
103 let content = output.to_mcp_content();
104 assert_eq!(content.len(), 1, "one block in, one block out");
105 assert_eq!(content[0]["type"], "image");
106 assert_eq!(content[0]["mimeType"], "image/png");
107 assert_eq!(content[0]["data"], "AP8Q", "the bytes base64-encoded, in order");
108 assert!(!output.is_error, "an image is a result, not a failure");
109 }
110
111 #[test]
114 fn text_and_failure_cross_as_text_blocks() {
115 let output = ToolOutput::error("no browser-capable client connected");
116 assert!(output.is_error, "the failure arm marks the call failed");
117 let content = output.to_mcp_content();
118 assert_eq!(content[0]["type"], "text");
119 assert_eq!(content[0]["text"], "no browser-capable client connected");
120 }
121}