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 should_defer: None,
131 always_load: None,
132 is_mcp: None,
133 search_hint: None,
134 aliases: None,
135 user_facing_name: None,
136 interrupt_behavior: None,
137 }
138}
139
140fn extract_schema_parts(
142 schema: &serde_json::Value,
143) -> (String, serde_json::Value, Option<Vec<String>>) {
144 let schema_type = schema
145 .get("type")
146 .and_then(|t| t.as_str())
147 .unwrap_or("object")
148 .to_string();
149
150 let properties = schema
151 .get("properties")
152 .cloned()
153 .unwrap_or(serde_json::json!({}));
154
155 let required = schema
156 .get("required")
157 .and_then(|r| r.as_array())
158 .map(|arr| {
159 arr.iter()
160 .filter_map(|s| s.as_str().map(String::from))
161 .collect()
162 });
163
164 (schema_type, properties, required)
165}
166
167pub fn text_content(text: &str) -> ContentBlock {
169 ContentBlock::Text {
170 content_type: "text".to_string(),
171 text: text.to_string(),
172 }
173}
174
175pub fn image_content(data: &str, mime_type: &str) -> ContentBlock {
177 ContentBlock::Image {
178 content_type: "image".to_string(),
179 data: data.to_string(),
180 mime_type: mime_type.to_string(),
181 }
182}
183
184pub fn resource_content(uri: &str, text: Option<&str>, blob: Option<&str>) -> ContentBlock {
186 ContentBlock::Resource {
187 content_type: "resource".to_string(),
188 resource: ResourceContent {
189 uri: uri.to_string(),
190 text: text.map(String::from),
191 blob: blob.map(String::from),
192 },
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn test_tool_annotations_default() {
202 let annotations = ToolAnnotations::default();
203 assert!(annotations.read_only_hint.is_none());
204 }
205
206 #[test]
207 fn test_tool_annotations_with_values() {
208 let annotations = ToolAnnotations {
209 read_only_hint: Some(true),
210 destructive_hint: Some(false),
211 idempotent_hint: Some(true),
212 open_world_hint: None,
213 };
214
215 assert_eq!(annotations.read_only_hint, Some(true));
216 assert_eq!(annotations.destructive_hint, Some(false));
217 }
218
219 #[test]
220 fn test_call_tool_result_text() {
221 let result = CallToolResult {
222 content: vec![text_content("Hello world")],
223 is_error: Some(false),
224 };
225
226 assert!(!result.is_error.unwrap());
227 if let ContentBlock::Text { text, .. } = &result.content[0] {
228 assert_eq!(text, "Hello world");
229 } else {
230 panic!("Expected Text content block");
231 }
232 }
233
234 #[test]
235 fn test_create_tool() {
236 let tool = create_tool(
237 "test_tool",
238 "A test tool",
239 serde_json::json!({
240 "type": "object",
241 "properties": {
242 "arg": { "type": "string" }
243 }
244 }),
245 );
246
247 assert_eq!(tool.name, "test_tool");
248 assert_eq!(tool.description, "A test tool");
249 }
250
251 #[test]
252 fn test_create_tool_with_annotations() {
253 let tool = create_tool_with_annotations(
254 "readonly_tool",
255 "A read-only tool",
256 serde_json::json!({
257 "type": "object",
258 "properties": {}
259 }),
260 ToolAnnotations {
261 read_only_hint: Some(true),
262 ..Default::default()
263 },
264 );
265
266 assert!(tool.annotations.is_some());
267 assert_eq!(tool.annotations.unwrap().read_only_hint, Some(true));
268 }
269
270 #[test]
271 fn test_sdk_tool_to_tool_definition() {
272 let sdk_tool = create_tool(
273 "weather",
274 "Get weather info",
275 serde_json::json!({
276 "type": "object",
277 "properties": {
278 "city": { "type": "string", "description": "City name" }
279 },
280 "required": ["city"]
281 }),
282 );
283
284 let tool_def = sdk_tool_to_tool_definition(sdk_tool);
285 assert_eq!(tool_def.name, "weather");
286 assert_eq!(tool_def.description, "Get weather info");
287 }
288
289 #[test]
290 fn test_extract_schema_parts() {
291 let schema = serde_json::json!({
292 "type": "object",
293 "properties": {
294 "name": { "type": "string" },
295 "age": { "type": "number" }
296 },
297 "required": ["name"]
298 });
299
300 let (schema_type, properties, required) = extract_schema_parts(&schema);
301
302 assert_eq!(schema_type, "object");
303 assert!(properties.get("name").is_some());
304 assert_eq!(required, Some(vec!["name".to_string()]));
305 }
306
307 #[test]
308 fn test_text_content_helper() {
309 let content = text_content("test");
310 match content {
311 ContentBlock::Text { content_type, text } => {
312 assert_eq!(content_type, "text");
313 assert_eq!(text, "test");
314 }
315 _ => panic!("Expected Text variant"),
316 }
317 }
318
319 #[test]
320 fn test_image_content_helper() {
321 let content = image_content("base64data", "image/png");
322 match content {
323 ContentBlock::Image {
324 content_type,
325 data,
326 mime_type,
327 } => {
328 assert_eq!(content_type, "image");
329 assert_eq!(data, "base64data");
330 assert_eq!(mime_type, "image/png");
331 }
332 _ => panic!("Expected Image variant"),
333 }
334 }
335
336 #[test]
337 fn test_resource_content_helper() {
338 let content = resource_content("file://test.txt", Some("content"), None);
339 match content {
340 ContentBlock::Resource {
341 content_type,
342 resource,
343 } => {
344 assert_eq!(content_type, "resource");
345 assert_eq!(resource.uri, "file://test.txt");
346 assert_eq!(resource.text, Some("content".to_string()));
347 }
348 _ => panic!("Expected Resource variant"),
349 }
350 }
351}