Skip to main content

umf/internal/
tool.rs

1//! Internal tool types for the Universal Message Format hub model.
2//!
3//! This module provides protocol-agnostic tool representations that serve
4//! as the canonical format for tool definitions, calls, and results.
5
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use std::collections::HashMap;
9
10/// Internal tool representation - the hub format for all protocols.
11///
12/// This type serves as the canonical representation for tools from any source:
13/// - Native tools (e.g., from CATS)
14/// - MCP tools (from Model Context Protocol servers)
15/// - A2A tools (from Agent-to-Agent protocol, future)
16///
17/// Protocol-specific metadata is preserved in the `metadata` field to enable
18/// round-trip conversion without data loss.
19///
20/// # Example
21///
22/// ```rust
23/// use umf::internal::InternalTool;
24/// use serde_json::json;
25///
26/// let tool = InternalTool::new(
27///     "get_weather",
28///     "Get the current weather for a location",
29///     json!({
30///         "type": "object",
31///         "properties": {
32///             "location": { "type": "string", "description": "City name" }
33///         },
34///         "required": ["location"]
35///     }),
36/// );
37/// ```
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
39pub struct InternalTool {
40    /// Tool name (unique identifier within a registry).
41    pub name: String,
42
43    /// Tool description for LLM consumption.
44    pub description: String,
45
46    /// JSON Schema for tool parameters.
47    ///
48    /// This should follow the JSON Schema specification and typically
49    /// has the structure:
50    /// ```json
51    /// {
52    ///     "type": "object",
53    ///     "properties": { ... },
54    ///     "required": [ ... ]
55    /// }
56    /// ```
57    pub parameters: Value,
58
59    /// Protocol-specific metadata preserved through conversion.
60    ///
61    /// Used to store protocol-specific fields that don't map directly
62    /// to the internal format, enabling lossless round-trip conversion.
63    /// Keys are prefixed with the protocol name (e.g., "mcp_annotations").
64    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
65    pub metadata: HashMap<String, Value>,
66}
67
68impl InternalTool {
69    /// Create a new internal tool.
70    pub fn new(name: impl Into<String>, description: impl Into<String>, parameters: Value) -> Self {
71        Self {
72            name: name.into(),
73            description: description.into(),
74            parameters,
75            metadata: HashMap::new(),
76        }
77    }
78
79    /// Add metadata to the tool.
80    pub fn with_metadata(mut self, key: impl Into<String>, value: Value) -> Self {
81        self.metadata.insert(key.into(), value);
82        self
83    }
84
85    /// Check if the tool has a specific metadata key.
86    pub fn has_metadata(&self, key: &str) -> bool {
87        self.metadata.contains_key(key)
88    }
89
90    /// Get a metadata value by key.
91    pub fn get_metadata(&self, key: &str) -> Option<&Value> {
92        self.metadata.get(key)
93    }
94}
95
96/// Internal tool call representation.
97///
98/// Represents a request to invoke a tool with specific arguments.
99#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
100pub struct InternalToolCall {
101    /// Unique identifier for this tool call.
102    ///
103    /// Used to correlate tool calls with their results.
104    pub id: String,
105
106    /// Name of the tool to invoke.
107    pub name: String,
108
109    /// Arguments to pass to the tool.
110    ///
111    /// This should match the tool's parameter schema.
112    pub arguments: Value,
113}
114
115impl InternalToolCall {
116    /// Create a new tool call.
117    pub fn new(id: impl Into<String>, name: impl Into<String>, arguments: Value) -> Self {
118        Self {
119            id: id.into(),
120            name: name.into(),
121            arguments,
122        }
123    }
124}
125
126/// Internal tool result representation.
127///
128/// Represents the result of a tool invocation.
129#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
130pub struct InternalToolResult {
131    /// ID of the tool call this is a result for.
132    pub tool_call_id: String,
133
134    /// Result content (typically stringified).
135    pub content: String,
136
137    /// Whether this result represents an error.
138    #[serde(default)]
139    pub is_error: bool,
140}
141
142impl InternalToolResult {
143    /// Create a successful tool result.
144    pub fn success(tool_call_id: impl Into<String>, content: impl Into<String>) -> Self {
145        Self {
146            tool_call_id: tool_call_id.into(),
147            content: content.into(),
148            is_error: false,
149        }
150    }
151
152    /// Create an error tool result.
153    pub fn error(tool_call_id: impl Into<String>, error: impl Into<String>) -> Self {
154        Self {
155            tool_call_id: tool_call_id.into(),
156            content: error.into(),
157            is_error: true,
158        }
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165    use serde_json::json;
166
167    #[test]
168    fn test_internal_tool_creation() {
169        let tool = InternalTool::new(
170            "get_weather",
171            "Get weather for a location",
172            json!({
173                "type": "object",
174                "properties": {
175                    "location": { "type": "string" }
176                },
177                "required": ["location"]
178            }),
179        );
180
181        assert_eq!(tool.name, "get_weather");
182        assert_eq!(tool.description, "Get weather for a location");
183        assert!(tool.metadata.is_empty());
184    }
185
186    #[test]
187    fn test_internal_tool_with_metadata() {
188        let tool = InternalTool::new("test", "Test tool", json!({}))
189            .with_metadata("mcp_annotations", json!({"readOnlyHint": true}));
190
191        assert!(tool.has_metadata("mcp_annotations"));
192        assert_eq!(
193            tool.get_metadata("mcp_annotations"),
194            Some(&json!({"readOnlyHint": true}))
195        );
196    }
197
198    #[test]
199    fn test_internal_tool_call() {
200        let call = InternalToolCall::new("call_123", "get_weather", json!({"location": "SF"}));
201
202        assert_eq!(call.id, "call_123");
203        assert_eq!(call.name, "get_weather");
204        assert_eq!(call.arguments["location"], "SF");
205    }
206
207    #[test]
208    fn test_internal_tool_result() {
209        let success = InternalToolResult::success("call_123", "72°F, sunny");
210        assert!(!success.is_error);
211        assert_eq!(success.content, "72°F, sunny");
212
213        let error = InternalToolResult::error("call_456", "Location not found");
214        assert!(error.is_error);
215        assert_eq!(error.content, "Location not found");
216    }
217
218    #[test]
219    fn test_internal_tool_serialization() {
220        let tool = InternalTool::new("test", "Test", json!({"type": "object"}))
221            .with_metadata("source", json!("mcp"));
222
223        let json = serde_json::to_string(&tool).unwrap();
224        let deserialized: InternalTool = serde_json::from_str(&json).unwrap();
225
226        assert_eq!(deserialized, tool);
227    }
228}