1use crate::types::ToolDefinition;
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Default, Serialize, Deserialize)]
6pub struct ToolAnnotations {
7 #[serde(rename = "readOnlyHint", skip_serializing_if = "Option::is_none")]
8 pub read_only_hint: Option<bool>,
9 #[serde(rename = "destructiveHint", skip_serializing_if = "Option::is_none")]
10 pub destructive_hint: Option<bool>,
11 #[serde(rename = "idempotentHint", skip_serializing_if = "Option::is_none")]
12 pub idempotent_hint: Option<bool>,
13 #[serde(rename = "openWorldHint", skip_serializing_if = "Option::is_none")]
14 pub open_world_hint: Option<bool>,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct CallToolResult {
20 pub content: Vec<ContentBlock>,
21 #[serde(skip_serializing_if = "Option::is_none")]
22 pub is_error: Option<bool>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26#[serde(untagged)]
27pub enum ContentBlock {
28 Text {
29 #[serde(rename = "type")]
30 content_type: String,
31 text: String,
32 },
33 Image {
34 #[serde(rename = "type")]
35 content_type: String,
36 data: String,
37 mime_type: String,
38 },
39 Resource {
40 #[serde(rename = "type")]
41 content_type: String,
42 resource: ResourceContent,
43 },
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct ResourceContent {
48 pub uri: String,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub text: Option<String>,
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub blob: Option<String>,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct SdkToolDefinition {
59 pub name: String,
60 pub description: String,
61 pub input_schema: serde_json::Value,
62 pub annotations: Option<ToolAnnotations>,
63}
64
65pub fn create_tool(
85 name: &str,
86 description: &str,
87 input_schema: serde_json::Value,
88) -> SdkToolDefinition {
89 SdkToolDefinition {
90 name: name.to_string(),
91 description: description.to_string(),
92 input_schema,
93 annotations: None,
94 }
95}
96
97pub fn create_tool_with_annotations(
99 name: &str,
100 description: &str,
101 input_schema: serde_json::Value,
102 annotations: ToolAnnotations,
103) -> SdkToolDefinition {
104 SdkToolDefinition {
105 name: name.to_string(),
106 description: description.to_string(),
107 input_schema,
108 annotations: Some(annotations),
109 }
110}
111
112pub fn sdk_tool_to_tool_definition(sdk_tool: SdkToolDefinition) -> ToolDefinition {
114 let tool_name = sdk_tool.name.clone();
115 let tool_description = sdk_tool.description.clone();
116 let input_schema = sdk_tool.input_schema.clone();
117
118 let (schema_type, properties, required) = extract_schema_parts(&input_schema);
120
121 crate::types::ToolDefinition {
122 name: tool_name,
123 description: tool_description,
124 input_schema: crate::types::ToolInputSchema {
125 schema_type,
126 properties,
127 required,
128 },
129 annotations: None,
130 }
131}
132
133fn extract_schema_parts(
135 schema: &serde_json::Value,
136) -> (String, serde_json::Value, Option<Vec<String>>) {
137 let schema_type = schema
138 .get("type")
139 .and_then(|t| t.as_str())
140 .unwrap_or("object")
141 .to_string();
142
143 let properties = schema
144 .get("properties")
145 .cloned()
146 .unwrap_or(serde_json::json!({}));
147
148 let required = schema
149 .get("required")
150 .and_then(|r| r.as_array())
151 .map(|arr| {
152 arr.iter()
153 .filter_map(|s| s.as_str().map(String::from))
154 .collect()
155 });
156
157 (schema_type, properties, required)
158}
159
160pub fn text_content(text: &str) -> ContentBlock {
162 ContentBlock::Text {
163 content_type: "text".to_string(),
164 text: text.to_string(),
165 }
166}
167
168pub fn image_content(data: &str, mime_type: &str) -> ContentBlock {
170 ContentBlock::Image {
171 content_type: "image".to_string(),
172 data: data.to_string(),
173 mime_type: mime_type.to_string(),
174 }
175}
176
177pub fn resource_content(uri: &str, text: Option<&str>, blob: Option<&str>) -> ContentBlock {
179 ContentBlock::Resource {
180 content_type: "resource".to_string(),
181 resource: ResourceContent {
182 uri: uri.to_string(),
183 text: text.map(String::from),
184 blob: blob.map(String::from),
185 },
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn test_tool_annotations_default() {
195 let annotations = ToolAnnotations::default();
196 assert!(annotations.read_only_hint.is_none());
197 }
198
199 #[test]
200 fn test_tool_annotations_with_values() {
201 let annotations = ToolAnnotations {
202 read_only_hint: Some(true),
203 destructive_hint: Some(false),
204 idempotent_hint: Some(true),
205 open_world_hint: None,
206 };
207
208 assert_eq!(annotations.read_only_hint, Some(true));
209 assert_eq!(annotations.destructive_hint, Some(false));
210 }
211
212 #[test]
213 fn test_call_tool_result_text() {
214 let result = CallToolResult {
215 content: vec![text_content("Hello world")],
216 is_error: Some(false),
217 };
218
219 assert!(!result.is_error.unwrap());
220 if let ContentBlock::Text { text, .. } = &result.content[0] {
221 assert_eq!(text, "Hello world");
222 } else {
223 panic!("Expected Text content block");
224 }
225 }
226
227 #[test]
228 fn test_create_tool() {
229 let tool = create_tool(
230 "test_tool",
231 "A test tool",
232 serde_json::json!({
233 "type": "object",
234 "properties": {
235 "arg": { "type": "string" }
236 }
237 }),
238 );
239
240 assert_eq!(tool.name, "test_tool");
241 assert_eq!(tool.description, "A test tool");
242 }
243
244 #[test]
245 fn test_create_tool_with_annotations() {
246 let tool = create_tool_with_annotations(
247 "readonly_tool",
248 "A read-only tool",
249 serde_json::json!({
250 "type": "object",
251 "properties": {}
252 }),
253 ToolAnnotations {
254 read_only_hint: Some(true),
255 ..Default::default()
256 },
257 );
258
259 assert!(tool.annotations.is_some());
260 assert_eq!(tool.annotations.unwrap().read_only_hint, Some(true));
261 }
262
263 #[test]
264 fn test_sdk_tool_to_tool_definition() {
265 let sdk_tool = create_tool(
266 "weather",
267 "Get weather info",
268 serde_json::json!({
269 "type": "object",
270 "properties": {
271 "city": { "type": "string", "description": "City name" }
272 },
273 "required": ["city"]
274 }),
275 );
276
277 let tool_def = sdk_tool_to_tool_definition(sdk_tool);
278 assert_eq!(tool_def.name, "weather");
279 assert_eq!(tool_def.description, "Get weather info");
280 }
281
282 #[test]
283 fn test_extract_schema_parts() {
284 let schema = serde_json::json!({
285 "type": "object",
286 "properties": {
287 "name": { "type": "string" },
288 "age": { "type": "number" }
289 },
290 "required": ["name"]
291 });
292
293 let (schema_type, properties, required) = extract_schema_parts(&schema);
294
295 assert_eq!(schema_type, "object");
296 assert!(properties.get("name").is_some());
297 assert_eq!(required, Some(vec!["name".to_string()]));
298 }
299
300 #[test]
301 fn test_text_content_helper() {
302 let content = text_content("test");
303 match content {
304 ContentBlock::Text { content_type, text } => {
305 assert_eq!(content_type, "text");
306 assert_eq!(text, "test");
307 }
308 _ => panic!("Expected Text variant"),
309 }
310 }
311
312 #[test]
313 fn test_image_content_helper() {
314 let content = image_content("base64data", "image/png");
315 match content {
316 ContentBlock::Image {
317 content_type,
318 data,
319 mime_type,
320 } => {
321 assert_eq!(content_type, "image");
322 assert_eq!(data, "base64data");
323 assert_eq!(mime_type, "image/png");
324 }
325 _ => panic!("Expected Image variant"),
326 }
327 }
328
329 #[test]
330 fn test_resource_content_helper() {
331 let content = resource_content("file://test.txt", Some("content"), None);
332 match content {
333 ContentBlock::Resource {
334 content_type,
335 resource,
336 } => {
337 assert_eq!(content_type, "resource");
338 assert_eq!(resource.uri, "file://test.txt");
339 assert_eq!(resource.text, Some("content".to_string()));
340 }
341 _ => panic!("Expected Resource variant"),
342 }
343 }
344}