reasonkit/mcp/
tools.rs

1//! MCP Tools
2//!
3//! Tool definitions, inputs, and results for MCP servers.
4
5use crate::error::Result;
6use async_trait::async_trait;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::collections::HashMap;
10use uuid::Uuid;
11
12/// Tool handler trait
13#[async_trait]
14pub trait ToolHandler: Send + Sync {
15    /// Execute the tool
16    async fn call(&self, arguments: HashMap<String, Value>) -> Result<ToolResult>;
17}
18
19/// Tool definition
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct Tool {
22    /// Tool name (must be unique per server)
23    pub name: String,
24
25    /// Human-readable description
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub description: Option<String>,
28
29    /// JSON Schema for input validation
30    #[serde(rename = "inputSchema")]
31    pub input_schema: Value,
32
33    /// Server ID (populated by registry)
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub server_id: Option<Uuid>,
36
37    /// Server name (populated by registry)
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub server_name: Option<String>,
40}
41
42/// Tool input
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct ToolInput {
45    /// Tool name
46    pub name: String,
47
48    /// Tool arguments (must match inputSchema)
49    #[serde(default)]
50    pub arguments: HashMap<String, Value>,
51}
52
53/// Tool result
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct ToolResult {
56    /// Result content
57    pub content: Vec<ToolResultContent>,
58
59    /// Whether the tool execution is finished
60    #[serde(skip_serializing_if = "Option::is_none", rename = "isError")]
61    pub is_error: Option<bool>,
62}
63
64/// Tool result content
65#[derive(Debug, Clone, Serialize, Deserialize)]
66#[serde(tag = "type", rename_all = "snake_case")]
67pub enum ToolResultContent {
68    /// Text content
69    Text {
70        /// Text data
71        text: String,
72    },
73    /// Image content
74    Image {
75        /// Image data (base64 encoded)
76        data: String,
77        /// MIME type
78        #[serde(rename = "mimeType")]
79        mime_type: String,
80    },
81    /// Resource reference
82    Resource {
83        /// Resource URI
84        uri: String,
85        /// MIME type
86        #[serde(skip_serializing_if = "Option::is_none", rename = "mimeType")]
87        mime_type: Option<String>,
88    },
89}
90
91/// Tool capability
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct ToolCapability {
94    /// Whether tool supports streaming results
95    #[serde(default)]
96    pub streaming: bool,
97
98    /// Whether tool supports cancellation
99    #[serde(default)]
100    pub cancellable: bool,
101
102    /// Estimated execution time in milliseconds
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub estimated_duration_ms: Option<u64>,
105}
106
107/// Resource template for dynamic resources
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct ResourceTemplate {
110    /// Template URI pattern (e.g., "file:///{path}")
111    #[serde(rename = "uriTemplate")]
112    pub uri_template: String,
113
114    /// Resource name
115    pub name: String,
116
117    /// Description
118    #[serde(skip_serializing_if = "Option::is_none")]
119    pub description: Option<String>,
120
121    /// MIME type
122    #[serde(skip_serializing_if = "Option::is_none", rename = "mimeType")]
123    pub mime_type: Option<String>,
124}
125
126/// Prompt definition
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct Prompt {
129    /// Prompt name
130    pub name: String,
131
132    /// Description
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub description: Option<String>,
135
136    /// Prompt arguments
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub arguments: Option<Vec<PromptArgument>>,
139}
140
141/// Prompt argument
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct PromptArgument {
144    /// Argument name
145    pub name: String,
146
147    /// Description
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub description: Option<String>,
150
151    /// Whether the argument is required
152    #[serde(default)]
153    pub required: bool,
154}
155
156/// Request to get a prompt
157#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct GetPromptRequest {
159    /// Prompt name
160    pub name: String,
161
162    /// Prompt arguments
163    #[serde(default)]
164    pub arguments: HashMap<String, String>,
165}
166
167/// Result of getting a prompt
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct GetPromptResult {
170    /// Prompt description (optional)
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub description: Option<String>,
173
174    /// Prompt messages
175    pub messages: Vec<PromptMessage>,
176}
177
178/// A message in a prompt
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct PromptMessage {
181    /// Message role (user, assistant, system)
182    pub role: String,
183
184    /// Message content
185    pub content: ToolResultContent,
186}
187
188impl Tool {
189    /// Create a simple tool with basic string input
190    pub fn simple(name: impl Into<String>, description: impl Into<String>) -> Self {
191        Self {
192            name: name.into(),
193            description: Some(description.into()),
194            input_schema: serde_json::json!({
195                "type": "object",
196                "properties": {},
197                "required": []
198            }),
199            server_id: None,
200            server_name: None,
201        }
202    }
203
204    /// Create a tool with custom input schema
205    pub fn with_schema(
206        name: impl Into<String>,
207        description: impl Into<String>,
208        schema: Value,
209    ) -> Self {
210        Self {
211            name: name.into(),
212            description: Some(description.into()),
213            input_schema: schema,
214            server_id: None,
215            server_name: None,
216        }
217    }
218}
219
220impl ToolResult {
221    /// Create a text result
222    pub fn text(text: impl Into<String>) -> Self {
223        Self {
224            content: vec![ToolResultContent::Text { text: text.into() }],
225            is_error: None,
226        }
227    }
228
229    /// Create an error result
230    pub fn error(message: impl Into<String>) -> Self {
231        Self {
232            content: vec![ToolResultContent::Text {
233                text: message.into(),
234            }],
235            is_error: Some(true),
236        }
237    }
238
239    /// Create a multi-content result
240    pub fn with_content(content: Vec<ToolResultContent>) -> Self {
241        Self {
242            content,
243            is_error: None,
244        }
245    }
246}
247
248impl ToolResultContent {
249    /// Create text content
250    pub fn text(text: impl Into<String>) -> Self {
251        Self::Text { text: text.into() }
252    }
253
254    /// Create image content
255    pub fn image(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
256        Self::Image {
257            data: data.into(),
258            mime_type: mime_type.into(),
259        }
260    }
261
262    /// Create resource reference
263    pub fn resource(uri: impl Into<String>) -> Self {
264        Self::Resource {
265            uri: uri.into(),
266            mime_type: None,
267        }
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    #[test]
276    fn test_simple_tool() {
277        let tool = Tool::simple("test_tool", "A test tool");
278        assert_eq!(tool.name, "test_tool");
279        assert!(tool.description.is_some());
280    }
281
282    #[test]
283    fn test_tool_result() {
284        let result = ToolResult::text("Success");
285        assert_eq!(result.content.len(), 1);
286        assert!(result.is_error.is_none());
287    }
288
289    #[test]
290    fn test_error_result() {
291        let result = ToolResult::error("Failed");
292        assert_eq!(result.is_error, Some(true));
293    }
294
295    #[test]
296    fn test_tool_serialization() {
297        let tool = Tool::simple("test", "description");
298        let json = serde_json::to_string(&tool).unwrap();
299        assert!(json.contains("\"name\":\"test\""));
300        assert!(json.contains("inputSchema"));
301    }
302}