turbomcp_protocol/types/tools.rs
1//! Types for the MCP tool-calling system.
2//!
3//! This module defines the data structures for defining tools, their input/output schemas,
4//! and the requests and responses used to list and execute them, as specified by the MCP standard.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9use super::{content::ContentBlock, core::Cursor};
10
11/// Optional metadata hints about a tool's behavior.
12///
13/// **Critical Warning** (from MCP spec):
14/// > "All properties in ToolAnnotations are **hints**. They are not guaranteed to
15/// > provide a faithful description of tool behavior. **Clients should never make
16/// > tool use decisions based on ToolAnnotations received from untrusted servers.**"
17///
18/// These fields are useful for UI display and general guidance, but should never
19/// be trusted for security decisions or behavioral assumptions.
20#[derive(Debug, Clone, Serialize, Deserialize, Default)]
21pub struct ToolAnnotations {
22 /// A user-friendly title for display in UIs (hint only).
23 #[serde(skip_serializing_if = "Option::is_none")]
24 pub title: Option<String>,
25 /// Role-based audience hint. Per spec, should be `"user"` or `"assistant"` (hint only).
26 #[serde(skip_serializing_if = "Option::is_none")]
27 pub audience: Option<Vec<String>>,
28 /// Subjective priority for UI sorting (hint only, often ignored).
29 #[serde(skip_serializing_if = "Option::is_none")]
30 pub priority: Option<f64>,
31 /// **Hint** that the tool may perform destructive actions (e.g., deleting data).
32 ///
33 /// Do not trust this for security decisions. Default: `true` if not specified.
34 #[serde(skip_serializing_if = "Option::is_none")]
35 #[serde(rename = "destructiveHint")]
36 pub destructive_hint: Option<bool>,
37 /// **Hint** that repeated calls with same args have no additional effects.
38 ///
39 /// Useful for retry logic, but verify actual behavior. Default: `false` if not specified.
40 #[serde(skip_serializing_if = "Option::is_none")]
41 #[serde(rename = "idempotentHint")]
42 pub idempotent_hint: Option<bool>,
43 /// **Hint** that the tool may interact with external systems or the real world.
44 ///
45 /// Do not trust this for sandboxing decisions. Default: `true` if not specified.
46 #[serde(skip_serializing_if = "Option::is_none")]
47 #[serde(rename = "openWorldHint")]
48 pub open_world_hint: Option<bool>,
49 /// **Hint** that the tool does not modify state (read-only).
50 ///
51 /// Do not trust this for security decisions. Default: `false` if not specified.
52 #[serde(skip_serializing_if = "Option::is_none")]
53 #[serde(rename = "readOnlyHint")]
54 pub read_only_hint: Option<bool>,
55 /// Custom application-specific hints.
56 #[serde(flatten)]
57 pub custom: HashMap<String, serde_json::Value>,
58}
59
60/// Represents a tool that can be executed by an MCP server, as per the MCP 2025-06-18 specification.
61///
62/// A `Tool` definition includes its programmatic name, a human-readable description,
63/// and JSON schemas for its inputs and outputs.
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct Tool {
66 /// The programmatic name of the tool, used to identify it in `CallToolRequest`.
67 pub name: String,
68
69 /// An optional, user-friendly title for the tool. Display name precedence is: `title`, `annotations.title`, then `name`.
70 #[serde(skip_serializing_if = "Option::is_none")]
71 pub title: Option<String>,
72
73 /// A human-readable description of what the tool does, which can be used by clients or LLMs.
74 #[serde(skip_serializing_if = "Option::is_none")]
75 pub description: Option<String>,
76
77 /// The JSON Schema object defining the parameters the tool accepts.
78 #[serde(rename = "inputSchema")]
79 pub input_schema: ToolInputSchema,
80
81 /// An optional JSON Schema object defining the structure of the tool's successful output.
82 #[serde(rename = "outputSchema", skip_serializing_if = "Option::is_none")]
83 pub output_schema: Option<ToolOutputSchema>,
84
85 /// Optional, additional metadata providing hints about the tool's behavior.
86 #[serde(skip_serializing_if = "Option::is_none")]
87 pub annotations: Option<ToolAnnotations>,
88
89 /// A general-purpose metadata field for custom data.
90 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
91 pub meta: Option<HashMap<String, serde_json::Value>>,
92}
93
94impl Default for Tool {
95 fn default() -> Self {
96 Self {
97 name: "unnamed_tool".to_string(), // Must have a valid name for MCP compliance
98 title: None,
99 description: None,
100 input_schema: ToolInputSchema::default(),
101 output_schema: None,
102 annotations: None,
103 meta: None,
104 }
105 }
106}
107
108impl Tool {
109 /// Creates a new `Tool` with a given name.
110 ///
111 /// # Panics
112 /// Panics if the name is empty or contains only whitespace.
113 pub fn new(name: impl Into<String>) -> Self {
114 let name = name.into();
115 assert!(!name.trim().is_empty(), "Tool name cannot be empty");
116 Self {
117 name,
118 title: None,
119 description: None,
120 input_schema: ToolInputSchema::default(),
121 output_schema: None,
122 annotations: None,
123 meta: None,
124 }
125 }
126
127 /// Creates a new `Tool` with a name and a description.
128 ///
129 /// # Panics
130 /// Panics if the name is empty or contains only whitespace.
131 pub fn with_description(name: impl Into<String>, description: impl Into<String>) -> Self {
132 let name = name.into();
133 assert!(!name.trim().is_empty(), "Tool name cannot be empty");
134 Self {
135 name,
136 title: None,
137 description: Some(description.into()),
138 input_schema: ToolInputSchema::default(),
139 output_schema: None,
140 annotations: None,
141 meta: None,
142 }
143 }
144
145 /// Sets the input schema for this tool.
146 ///
147 /// # Example
148 /// ```
149 /// # use turbomcp_protocol::types::{Tool, ToolInputSchema};
150 /// let schema = ToolInputSchema::empty();
151 /// let tool = Tool::new("my_tool").with_input_schema(schema);
152 /// ```
153 pub fn with_input_schema(mut self, schema: ToolInputSchema) -> Self {
154 self.input_schema = schema;
155 self
156 }
157
158 /// Sets the output schema for this tool.
159 pub fn with_output_schema(mut self, schema: ToolOutputSchema) -> Self {
160 self.output_schema = Some(schema);
161 self
162 }
163
164 /// Sets the user-friendly title for this tool.
165 pub fn with_title(mut self, title: impl Into<String>) -> Self {
166 self.title = Some(title.into());
167 self
168 }
169
170 /// Sets the annotations for this tool.
171 pub fn with_annotations(mut self, annotations: ToolAnnotations) -> Self {
172 self.annotations = Some(annotations);
173 self
174 }
175}
176
177/// Defines the structure of the arguments a tool accepts, as a JSON Schema object.
178#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct ToolInputSchema {
180 /// The type of the schema, which must be "object" for tool inputs.
181 #[serde(rename = "type")]
182 pub schema_type: String,
183 /// A map defining the properties (parameters) the tool accepts.
184 #[serde(skip_serializing_if = "Option::is_none")]
185 pub properties: Option<HashMap<String, serde_json::Value>>,
186 /// A list of property names that are required.
187 #[serde(skip_serializing_if = "Option::is_none")]
188 pub required: Option<Vec<String>>,
189 /// Whether additional, unspecified properties are allowed.
190 #[serde(
191 rename = "additionalProperties",
192 skip_serializing_if = "Option::is_none"
193 )]
194 pub additional_properties: Option<bool>,
195}
196
197impl Default for ToolInputSchema {
198 /// Creates a default `ToolInputSchema` that accepts an empty object.
199 fn default() -> Self {
200 Self {
201 schema_type: "object".to_string(),
202 properties: None,
203 required: None,
204 additional_properties: None,
205 }
206 }
207}
208
209impl ToolInputSchema {
210 /// Creates a new, empty input schema that accepts no parameters.
211 pub fn empty() -> Self {
212 Self::default()
213 }
214
215 /// Creates a new schema with a given set of properties.
216 pub fn with_properties(properties: HashMap<String, serde_json::Value>) -> Self {
217 Self {
218 schema_type: "object".to_string(),
219 properties: Some(properties),
220 required: None,
221 additional_properties: None,
222 }
223 }
224
225 /// Creates a new schema with a given set of properties and a list of required properties.
226 pub fn with_required_properties(
227 properties: HashMap<String, serde_json::Value>,
228 required: Vec<String>,
229 ) -> Self {
230 Self {
231 schema_type: "object".to_string(),
232 properties: Some(properties),
233 required: Some(required),
234 additional_properties: Some(false),
235 }
236 }
237
238 /// Adds a property to the schema using a builder pattern.
239 ///
240 /// # Example
241 /// ```
242 /// # use turbomcp_protocol::types::ToolInputSchema;
243 /// # use serde_json::json;
244 /// let schema = ToolInputSchema::empty()
245 /// .add_property("name".to_string(), json!({ "type": "string" }));
246 /// ```
247 pub fn add_property(mut self, name: String, property: serde_json::Value) -> Self {
248 self.properties
249 .get_or_insert_with(HashMap::new)
250 .insert(name, property);
251 self
252 }
253
254 /// Marks a property as required using a builder pattern.
255 ///
256 /// # Example
257 /// ```
258 /// # use turbomcp_protocol::types::ToolInputSchema;
259 /// # use serde_json::json;
260 /// let schema = ToolInputSchema::empty()
261 /// .add_property("name".to_string(), json!({ "type": "string" }))
262 /// .require_property("name".to_string());
263 /// ```
264 pub fn require_property(mut self, name: String) -> Self {
265 let required = self.required.get_or_insert_with(Vec::new);
266 if !required.contains(&name) {
267 required.push(name);
268 }
269 self
270 }
271}
272
273/// Defines the structure of a tool's successful output, as a JSON Schema object.
274#[derive(Debug, Clone, Serialize, Deserialize)]
275pub struct ToolOutputSchema {
276 /// The type of the schema, which must be "object" for tool outputs.
277 #[serde(rename = "type")]
278 pub schema_type: String,
279 /// A map defining the properties of the output object.
280 #[serde(skip_serializing_if = "Option::is_none")]
281 pub properties: Option<HashMap<String, serde_json::Value>>,
282 /// A list of property names in the output that are required.
283 #[serde(skip_serializing_if = "Option::is_none")]
284 pub required: Option<Vec<String>>,
285 /// Whether additional, unspecified properties are allowed in the output.
286 #[serde(
287 rename = "additionalProperties",
288 skip_serializing_if = "Option::is_none"
289 )]
290 pub additional_properties: Option<bool>,
291}
292
293/// A request to list the available tools on a server.
294#[derive(Debug, Clone, Serialize, Deserialize, Default)]
295pub struct ListToolsRequest {
296 /// An optional cursor for pagination. If provided, the server should return
297 /// the next page of results starting after this cursor.
298 #[serde(skip_serializing_if = "Option::is_none")]
299 pub cursor: Option<Cursor>,
300 /// Optional metadata for the request.
301 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
302 pub _meta: Option<serde_json::Value>,
303}
304
305/// The result of a `ListToolsRequest`.
306#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct ListToolsResult {
308 /// The list of available tools for the current page.
309 pub tools: Vec<Tool>,
310 /// An optional continuation token for retrieving the next page of results.
311 /// If `None`, there are no more results.
312 #[serde(rename = "nextCursor", skip_serializing_if = "Option::is_none")]
313 pub next_cursor: Option<Cursor>,
314 /// Optional metadata for the result.
315 #[serde(skip_serializing_if = "Option::is_none")]
316 pub _meta: Option<serde_json::Value>,
317}
318
319/// A request to execute a specific tool.
320#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct CallToolRequest {
322 /// The programmatic name of the tool to call.
323 pub name: String,
324 /// The arguments to pass to the tool, conforming to its `input_schema`.
325 #[serde(skip_serializing_if = "Option::is_none")]
326 pub arguments: Option<HashMap<String, serde_json::Value>>,
327 /// Optional metadata for the request.
328 #[serde(skip_serializing_if = "Option::is_none")]
329 pub _meta: Option<serde_json::Value>,
330}
331
332/// The result of a `CallToolRequest`.
333#[derive(Debug, Clone, Serialize, Deserialize)]
334pub struct CallToolResult {
335 /// The output of the tool, typically as a series of text or other content blocks. This is required.
336 pub content: Vec<ContentBlock>,
337 /// An optional boolean indicating whether the tool execution resulted in an error.
338 ///
339 /// When `is_error` is `true`, all content blocks should be treated as error information.
340 /// The error message may span multiple text blocks for structured error reporting.
341 #[serde(rename = "isError", skip_serializing_if = "Option::is_none")]
342 pub is_error: Option<bool>,
343 /// Optional structured output from the tool, conforming to its `output_schema`.
344 ///
345 /// When present, this contains schema-validated JSON output that clients can parse
346 /// and use programmatically. Tools that return structured content SHOULD also include
347 /// the serialized JSON in a TextContent block for backward compatibility with clients
348 /// that don't support structured output.
349 ///
350 /// See [`Tool::output_schema`] for defining the expected structure.
351 #[serde(rename = "structuredContent", skip_serializing_if = "Option::is_none")]
352 pub structured_content: Option<serde_json::Value>,
353 /// Optional metadata for the result.
354 ///
355 /// This field is for client applications and tools to pass additional context that
356 /// should NOT be exposed to LLMs. Examples include tracking IDs, performance metrics,
357 /// cache status, or internal state information.
358 #[serde(skip_serializing_if = "Option::is_none")]
359 pub _meta: Option<serde_json::Value>,
360}
361
362impl CallToolResult {
363 /// Extracts and concatenates all text content from the result.
364 ///
365 /// This is useful for simple text-only tools or when you want to present
366 /// all textual output as a single string.
367 ///
368 /// # Returns
369 ///
370 /// A single string containing all text blocks concatenated with newlines.
371 /// Returns an empty string if there are no text blocks.
372 ///
373 /// # Example
374 ///
375 /// ```rust
376 /// use turbomcp_protocol::types::{CallToolResult, ContentBlock, TextContent};
377 ///
378 /// let result = CallToolResult {
379 /// content: vec![
380 /// ContentBlock::Text(TextContent {
381 /// text: "Line 1".to_string(),
382 /// annotations: None,
383 /// meta: None,
384 /// }),
385 /// ContentBlock::Text(TextContent {
386 /// text: "Line 2".to_string(),
387 /// annotations: None,
388 /// meta: None,
389 /// }),
390 /// ],
391 /// is_error: None,
392 /// structured_content: None,
393 /// _meta: None,
394 /// };
395 ///
396 /// assert_eq!(result.all_text(), "Line 1\nLine 2");
397 /// ```
398 pub fn all_text(&self) -> String {
399 self.content
400 .iter()
401 .filter_map(|block| match block {
402 ContentBlock::Text(text) => Some(text.text.as_str()),
403 _ => None,
404 })
405 .collect::<Vec<_>>()
406 .join("\n")
407 }
408
409 /// Returns the text content of the first text block, if any.
410 ///
411 /// This is a common pattern for simple tools that return a single text response.
412 ///
413 /// # Returns
414 ///
415 /// `Some(&str)` if the first content block is text, `None` otherwise.
416 ///
417 /// # Example
418 ///
419 /// ```rust
420 /// use turbomcp_protocol::types::{CallToolResult, ContentBlock, TextContent};
421 ///
422 /// let result = CallToolResult {
423 /// content: vec![
424 /// ContentBlock::Text(TextContent {
425 /// text: "Hello, world!".to_string(),
426 /// annotations: None,
427 /// meta: None,
428 /// }),
429 /// ],
430 /// is_error: None,
431 /// structured_content: None,
432 /// _meta: None,
433 /// };
434 ///
435 /// assert_eq!(result.first_text(), Some("Hello, world!"));
436 /// ```
437 pub fn first_text(&self) -> Option<&str> {
438 self.content.first().and_then(|block| match block {
439 ContentBlock::Text(text) => Some(text.text.as_str()),
440 _ => None,
441 })
442 }
443
444 /// Checks if the tool execution resulted in an error.
445 ///
446 /// # Returns
447 ///
448 /// `true` if `is_error` is explicitly set to `true`, `false` otherwise
449 /// (including when `is_error` is `None`).
450 ///
451 /// # Example
452 ///
453 /// ```rust
454 /// use turbomcp_protocol::types::CallToolResult;
455 ///
456 /// let success_result = CallToolResult {
457 /// content: vec![],
458 /// is_error: Some(false),
459 /// structured_content: None,
460 /// _meta: None,
461 /// };
462 /// assert!(!success_result.has_error());
463 ///
464 /// let error_result = CallToolResult {
465 /// content: vec![],
466 /// is_error: Some(true),
467 /// structured_content: None,
468 /// _meta: None,
469 /// };
470 /// assert!(error_result.has_error());
471 ///
472 /// let unspecified_result = CallToolResult {
473 /// content: vec![],
474 /// is_error: None,
475 /// structured_content: None,
476 /// _meta: None,
477 /// };
478 /// assert!(!unspecified_result.has_error());
479 /// ```
480 pub fn has_error(&self) -> bool {
481 self.is_error.unwrap_or(false)
482 }
483
484 /// Creates a user-friendly display string for the tool result.
485 ///
486 /// This method provides a formatted representation suitable for logging,
487 /// debugging, or displaying to end users. It handles multiple content types
488 /// and includes structured content and error information when present.
489 ///
490 /// # Returns
491 ///
492 /// A formatted string representing the tool result.
493 ///
494 /// # Example
495 ///
496 /// ```rust
497 /// use turbomcp_protocol::types::{CallToolResult, ContentBlock, TextContent};
498 ///
499 /// let result = CallToolResult {
500 /// content: vec![
501 /// ContentBlock::Text(TextContent {
502 /// text: "Operation completed".to_string(),
503 /// annotations: None,
504 /// meta: None,
505 /// }),
506 /// ],
507 /// is_error: Some(false),
508 /// structured_content: None,
509 /// _meta: None,
510 /// };
511 ///
512 /// let display = result.to_display_string();
513 /// assert!(display.contains("Operation completed"));
514 /// ```
515 pub fn to_display_string(&self) -> String {
516 let mut parts = Vec::new();
517
518 // Add error indicator if present
519 if self.has_error() {
520 parts.push("ERROR:".to_string());
521 }
522
523 // Process content blocks
524 for (i, block) in self.content.iter().enumerate() {
525 match block {
526 ContentBlock::Text(text) => {
527 parts.push(text.text.clone());
528 }
529 ContentBlock::Image(img) => {
530 parts.push(format!(
531 "[Image: {} bytes, type: {}]",
532 img.data.len(),
533 img.mime_type
534 ));
535 }
536 ContentBlock::Audio(audio) => {
537 parts.push(format!(
538 "[Audio: {} bytes, type: {}]",
539 audio.data.len(),
540 audio.mime_type
541 ));
542 }
543 ContentBlock::ResourceLink(link) => {
544 let desc = link.description.as_deref().unwrap_or("");
545 let mime = link
546 .mime_type
547 .as_deref()
548 .map(|m| format!(" [{}]", m))
549 .unwrap_or_default();
550 parts.push(format!(
551 "[Resource: {}{}{}{}]",
552 link.name,
553 mime,
554 if !desc.is_empty() { ": " } else { "" },
555 desc
556 ));
557 }
558 ContentBlock::Resource(_resource) => {
559 parts.push(format!("[Embedded Resource #{}]", i + 1));
560 }
561 }
562 }
563
564 // Add structured content indicator if present
565 if self.structured_content.is_some() {
566 parts.push("[Includes structured output]".to_string());
567 }
568
569 parts.join("\n")
570 }
571}