Skip to main content

mcp_execution_server/
types.rs

1//! Type definitions for MCP server tools.
2//!
3//! This module defines all parameter and result types for the three main tools:
4//! - `introspect_server`: Connect to and introspect an MCP server
5//! - `save_categorized_tools`: Generate TypeScript files with categorization
6//! - `list_generated_servers`: List all servers with generated files
7
8use crate::clock::Clock;
9use chrono::{DateTime, Utc};
10use mcp_execution_core::{ServerConfig, ServerId};
11use mcp_execution_introspector::ServerInfo;
12use schemars::JsonSchema;
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15use std::path::PathBuf;
16use uuid::Uuid;
17
18// ============================================================================
19// introspect_server types
20// ============================================================================
21
22/// Parameters for introspecting an MCP server.
23///
24/// # Examples
25///
26/// ```
27/// use mcp_execution_server::types::IntrospectServerParams;
28/// use std::collections::HashMap;
29///
30/// let params = IntrospectServerParams {
31///     server_id: "github".to_string(),
32///     command: "npx".to_string(),
33///     args: vec!["-y".to_string(), "@anthropic/mcp-server-github".to_string()],
34///     env: HashMap::new(),
35///     output_dir: None,
36///     connect_timeout_secs: None,
37///     discover_timeout_secs: None,
38/// };
39/// ```
40#[derive(Debug, Clone, Deserialize, JsonSchema)]
41pub struct IntrospectServerParams {
42    /// Unique identifier for the server (e.g., "github", "filesystem")
43    pub server_id: String,
44
45    /// Command to start the server (e.g., "npx", "docker")
46    pub command: String,
47
48    /// Arguments to pass to the command
49    #[serde(default)]
50    pub args: Vec<String>,
51
52    /// Environment variables for the server process
53    #[serde(default)]
54    pub env: HashMap<String, String>,
55
56    /// Custom output directory (default: `~/.claude/servers/{server_id}`)
57    pub output_dir: Option<PathBuf>,
58
59    /// Connection (handshake) timeout in seconds, overriding the 30-second
60    /// default when set.
61    #[serde(default)]
62    pub connect_timeout_secs: Option<u64>,
63
64    /// Tool discovery timeout in seconds, overriding the 30-second default
65    /// when set.
66    #[serde(default)]
67    pub discover_timeout_secs: Option<u64>,
68}
69
70/// Result from introspecting an MCP server.
71///
72/// Contains tool metadata for Claude to categorize and a session ID
73/// for use with `save_categorized_tools`.
74#[derive(Debug, Clone, Serialize, JsonSchema)]
75pub struct IntrospectServerResult {
76    /// Server identifier
77    pub server_id: String,
78
79    /// Human-readable server name
80    pub server_name: String,
81
82    /// Number of tools discovered
83    pub tools_found: usize,
84
85    /// List of tools for categorization
86    pub tools: Vec<IntrospectedToolSummary>,
87
88    /// Session ID for `save_categorized_tools` call
89    pub session_id: Uuid,
90
91    /// Session expiration time (ISO 8601)
92    pub expires_at: DateTime<Utc>,
93}
94
95/// Summary of an introspected tool, returned to Claude for categorization.
96///
97/// Includes the tool name, description, and parameter names to help
98/// Claude understand the tool's purpose and assign appropriate categories.
99#[derive(Debug, Clone, Serialize, JsonSchema)]
100pub struct IntrospectedToolSummary {
101    /// Original tool name
102    pub name: String,
103
104    /// Tool description from server
105    pub description: String,
106
107    /// Parameter names for context
108    pub parameters: Vec<String>,
109}
110
111// ============================================================================
112// save_categorized_tools types
113// ============================================================================
114
115/// Parameters for saving categorized tools.
116///
117/// # Examples
118///
119/// ```
120/// use mcp_execution_server::types::{SaveCategorizedToolsParams, CategorizedTool};
121/// use uuid::Uuid;
122///
123/// let params = SaveCategorizedToolsParams {
124///     session_id: Uuid::new_v4(),
125///     categorized_tools: vec![
126///         CategorizedTool {
127///             name: "create_issue".to_string(),
128///             category: "issues".to_string(),
129///             keywords: "create,issue,new,bug,feature".to_string(),
130///             short_description: "Create a new issue in a repository".to_string(),
131///         },
132///     ],
133/// };
134/// ```
135#[derive(Debug, Clone, Deserialize, JsonSchema)]
136pub struct SaveCategorizedToolsParams {
137    /// Session ID from `introspect_server` call
138    pub session_id: Uuid,
139
140    /// Tools with Claude's categorization
141    pub categorized_tools: Vec<CategorizedTool>,
142}
143
144/// A tool with categorization metadata from Claude.
145///
146/// Claude analyzes the tool's purpose and provides:
147/// - A category for grouping related tools
148/// - Keywords for discovery via grep/search
149/// - A concise description for file headers
150#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
151pub struct CategorizedTool {
152    /// Original tool name (must match introspected tool)
153    pub name: String,
154
155    /// Category assigned by Claude (e.g., "issues", "repos", "users")
156    pub category: String,
157
158    /// Comma-separated keywords for discovery
159    pub keywords: String,
160
161    /// Concise description (max 80 chars) for header comment
162    pub short_description: String,
163}
164
165/// Result from saving categorized tools.
166///
167/// Reports success status, number of files generated, and any errors
168/// that occurred during generation.
169#[derive(Debug, Clone, Serialize, JsonSchema)]
170pub struct SaveCategorizedToolsResult {
171    /// Whether generation succeeded
172    pub success: bool,
173
174    /// Number of TypeScript files created
175    pub files_generated: usize,
176
177    /// Directory where files were written
178    pub output_dir: String,
179
180    /// Count of tools per category
181    pub categories: HashMap<String, usize>,
182
183    /// Any tools that failed to generate
184    #[serde(skip_serializing_if = "Vec::is_empty")]
185    pub errors: Vec<ToolGenerationError>,
186}
187
188/// Error that occurred while generating a specific tool.
189#[derive(Debug, Clone, Serialize, JsonSchema)]
190pub struct ToolGenerationError {
191    /// Name of the tool that failed
192    pub tool_name: String,
193
194    /// Error message
195    pub error: String,
196}
197
198// ============================================================================
199// list_generated_servers types
200// ============================================================================
201
202/// Parameters for listing generated servers.
203#[derive(Debug, Clone, Deserialize, JsonSchema)]
204pub struct ListGeneratedServersParams {
205    /// Base directory to scan (default: `~/.claude/servers`)
206    pub base_dir: Option<String>,
207}
208
209/// Result from listing generated servers.
210#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
211pub struct ListGeneratedServersResult {
212    /// List of servers with generated files
213    pub servers: Vec<GeneratedServerInfo>,
214
215    /// Total number of servers found
216    pub total_servers: usize,
217}
218
219/// Information about a server with generated progressive loading files.
220#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
221pub struct GeneratedServerInfo {
222    /// Server identifier
223    pub id: String,
224
225    /// Number of tool files (excluding runtime)
226    pub tool_count: usize,
227
228    /// Last generation timestamp
229    pub generated_at: Option<DateTime<Utc>>,
230
231    /// Directory path
232    pub output_dir: String,
233}
234
235// ============================================================================
236// State management types
237// ============================================================================
238
239/// Pending generation session.
240///
241/// Stores introspection data between `introspect_server` and
242/// `save_categorized_tools` calls.
243#[derive(Debug, Clone)]
244pub struct PendingGeneration {
245    /// Server identifier
246    pub server_id: ServerId,
247
248    /// Full server introspection data
249    pub server_info: ServerInfo,
250
251    /// Server configuration for regeneration if needed
252    pub config: ServerConfig,
253
254    /// Output directory
255    pub output_dir: PathBuf,
256
257    /// Session creation time
258    pub created_at: DateTime<Utc>,
259
260    /// Session expiration time (30 minutes default)
261    pub expires_at: DateTime<Utc>,
262}
263
264impl PendingGeneration {
265    /// Default session timeout: 30 minutes.
266    pub const DEFAULT_TIMEOUT_MINUTES: i64 = 30;
267
268    /// Creates a new pending generation session.
269    ///
270    /// The session's `created_at`/`expires_at` are derived from `clock.now()`,
271    /// so tests can inject a fake clock instead of rewinding `expires_at`
272    /// after construction. Production callers should pass [`SystemClock`](crate::clock::SystemClock).
273    ///
274    /// # Examples
275    ///
276    /// ```
277    /// use mcp_execution_server::types::PendingGeneration;
278    /// use mcp_execution_server::clock::SystemClock;
279    /// use mcp_execution_core::{ServerId, ServerConfig};
280    /// use mcp_execution_introspector::ServerInfo;
281    /// use std::path::PathBuf;
282    ///
283    /// # fn example(server_info: ServerInfo) {
284    /// let server_id = ServerId::new("github");
285    /// let config = ServerConfig::builder()
286    ///     .command("npx".to_string())
287    ///     .arg("-y".to_string())
288    ///     .arg("@anthropic/mcp-server-github".to_string())
289    ///     .build();
290    /// let output_dir = PathBuf::from("/tmp/output");
291    ///
292    /// let pending = PendingGeneration::new(
293    ///     server_id,
294    ///     server_info,
295    ///     config,
296    ///     output_dir,
297    ///     &SystemClock,
298    /// );
299    /// # }
300    /// ```
301    #[must_use]
302    pub fn new(
303        server_id: ServerId,
304        server_info: ServerInfo,
305        config: ServerConfig,
306        output_dir: PathBuf,
307        clock: &dyn Clock,
308    ) -> Self {
309        let now = clock.now();
310        Self {
311            server_id,
312            server_info,
313            config,
314            output_dir,
315            created_at: now,
316            expires_at: now + chrono::Duration::minutes(Self::DEFAULT_TIMEOUT_MINUTES),
317        }
318    }
319
320    /// Checks if this session has expired, using `clock.now()` as the current time.
321    ///
322    /// # Examples
323    ///
324    /// ```
325    /// use mcp_execution_server::types::PendingGeneration;
326    /// use mcp_execution_server::clock::SystemClock;
327    /// # use mcp_execution_core::{ServerId, ServerConfig};
328    /// # use mcp_execution_introspector::ServerInfo;
329    /// # use std::path::PathBuf;
330    ///
331    /// # fn example(server_info: ServerInfo) {
332    /// let pending = PendingGeneration::new(
333    ///     ServerId::new("test"),
334    ///     server_info,
335    ///     ServerConfig::builder().command("echo".to_string()).build(),
336    ///     PathBuf::from("/tmp"),
337    ///     &SystemClock,
338    /// );
339    ///
340    /// assert!(!pending.is_expired(&SystemClock));
341    /// # }
342    /// ```
343    #[must_use]
344    pub fn is_expired(&self, clock: &dyn Clock) -> bool {
345        clock.now() > self.expires_at
346    }
347}
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352    use crate::clock::{SystemClock, TestClock};
353
354    #[test]
355    fn test_pending_generation_not_expired() {
356        let pending = create_test_pending();
357        assert!(!pending.is_expired(&SystemClock));
358    }
359
360    #[test]
361    fn test_pending_generation_not_expired_at_exact_boundary() {
362        let clock = TestClock::new(Utc::now());
363        let pending = create_test_pending_with_clock(&clock);
364
365        // `is_expired` uses strict `>`, so the exact expiry instant is not expired.
366        clock.advance(chrono::Duration::minutes(
367            PendingGeneration::DEFAULT_TIMEOUT_MINUTES,
368        ));
369        assert!(!pending.is_expired(&clock));
370    }
371
372    #[test]
373    fn test_pending_generation_not_expired_one_second_before_boundary() {
374        let clock = TestClock::new(Utc::now());
375        let pending = create_test_pending_with_clock(&clock);
376
377        clock.advance(
378            chrono::Duration::minutes(PendingGeneration::DEFAULT_TIMEOUT_MINUTES)
379                - chrono::Duration::seconds(1),
380        );
381        assert!(!pending.is_expired(&clock));
382    }
383
384    #[test]
385    fn test_pending_generation_expired_one_second_after_boundary() {
386        let clock = TestClock::new(Utc::now());
387        let pending = create_test_pending_with_clock(&clock);
388
389        clock.advance(
390            chrono::Duration::minutes(PendingGeneration::DEFAULT_TIMEOUT_MINUTES)
391                + chrono::Duration::seconds(1),
392        );
393        assert!(pending.is_expired(&clock));
394    }
395
396    #[test]
397    fn test_categorized_tool_serialization() {
398        let tool = CategorizedTool {
399            name: "create_issue".to_string(),
400            category: "issues".to_string(),
401            keywords: "create,issue,new".to_string(),
402            short_description: "Create a new issue".to_string(),
403        };
404
405        let json = serde_json::to_string(&tool).unwrap();
406        let _deserialized: CategorizedTool = serde_json::from_str(&json).unwrap();
407    }
408
409    // Test helpers
410    fn create_test_pending() -> PendingGeneration {
411        create_test_pending_with_clock(&SystemClock)
412    }
413
414    fn create_test_pending_with_clock(clock: &dyn Clock) -> PendingGeneration {
415        use mcp_execution_core::ToolName;
416        use mcp_execution_introspector::{ServerCapabilities, ToolInfo};
417
418        let server_id = ServerId::new("test");
419        let server_info = ServerInfo {
420            id: server_id.clone(),
421            name: "Test Server".to_string(),
422            version: "1.0.0".to_string(),
423            capabilities: ServerCapabilities {
424                supports_tools: true,
425                supports_resources: false,
426                supports_prompts: false,
427            },
428            tools: vec![ToolInfo {
429                name: ToolName::new("test_tool"),
430                description: "Test tool description".to_string(),
431                input_schema: serde_json::json!({}),
432                output_schema: None,
433            }],
434        };
435        let config = ServerConfig::builder().command("echo".to_string()).build();
436        let output_dir = PathBuf::from("/tmp/test");
437
438        PendingGeneration::new(server_id, server_info, config, output_dir, clock)
439    }
440}