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}