Skip to main content

adk_tool/mcp/manager/
config.rs

1//! MCP server configuration types.
2//!
3//! Defines [`McpServerConfig`], [`RestartPolicy`], and the internal [`McpJsonFile`]
4//! for parsing Kiro's `mcp.json` format.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// Configuration for a single managed MCP server.
10///
11/// Deserialized from Kiro's `mcp.json` format with camelCase field names.
12///
13/// # Example
14///
15/// ```rust
16/// use adk_tool::mcp::manager::McpServerConfig;
17///
18/// let json = r#"{
19///     "command": "npx",
20///     "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
21///     "env": {},
22///     "disabled": false,
23///     "autoApprove": ["read_file"]
24/// }"#;
25///
26/// let config: McpServerConfig = serde_json::from_str(json).unwrap();
27/// assert_eq!(config.command, "npx");
28/// assert!(!config.disabled);
29/// ```
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
31#[serde(rename_all = "camelCase")]
32pub struct McpServerConfig {
33    /// Executable command to spawn the MCP server process.
34    pub command: String,
35
36    /// Command-line arguments passed to the server process.
37    #[serde(default)]
38    pub args: Vec<String>,
39
40    /// Environment variables set for the server process.
41    #[serde(default)]
42    pub env: HashMap<String, String>,
43
44    /// When `true`, the manager skips starting this server and sets its status to `Disabled`.
45    #[serde(default)]
46    pub disabled: bool,
47
48    /// Tool names pre-approved for execution without confirmation.
49    #[serde(default)]
50    pub auto_approve: Vec<String>,
51
52    /// Optional restart policy controlling auto-restart behavior with exponential backoff.
53    #[serde(default)]
54    pub restart_policy: Option<RestartPolicy>,
55}
56
57/// Controls auto-restart behavior with exponential backoff.
58///
59/// When a managed server crashes, the manager uses this policy to determine
60/// how long to wait before restarting and when to give up.
61///
62/// # Backoff Formula
63///
64/// ```text
65/// delay(attempt) = min(initial_delay_ms × backoff_multiplier ^ attempt, max_delay_ms)
66/// ```
67///
68/// # Example
69///
70/// ```rust
71/// use adk_tool::mcp::manager::RestartPolicy;
72///
73/// let json = r#"{
74///     "initialDelayMs": 500,
75///     "maxDelayMs": 15000,
76///     "backoffMultiplier": 2.0,
77///     "maxRestartAttempts": 5
78/// }"#;
79///
80/// let policy: RestartPolicy = serde_json::from_str(json).unwrap();
81/// assert_eq!(policy.initial_delay_ms, 500);
82/// assert_eq!(policy.max_restart_attempts, 5);
83/// ```
84#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
85#[serde(rename_all = "camelCase")]
86pub struct RestartPolicy {
87    /// Initial delay in milliseconds before the first restart attempt. Default: 1000.
88    #[serde(default = "default_initial_delay")]
89    pub initial_delay_ms: u64,
90
91    /// Maximum delay in milliseconds between restart attempts. Default: 30000.
92    #[serde(default = "default_max_delay")]
93    pub max_delay_ms: u64,
94
95    /// Multiplier applied to the delay after each failed attempt. Default: 2.0.
96    #[serde(default = "default_backoff_multiplier")]
97    pub backoff_multiplier: f64,
98
99    /// Maximum number of consecutive restart attempts before giving up. Default: 10.
100    #[serde(default = "default_max_restart_attempts")]
101    pub max_restart_attempts: u32,
102}
103
104fn default_initial_delay() -> u64 {
105    1000
106}
107
108fn default_max_delay() -> u64 {
109    30000
110}
111
112fn default_backoff_multiplier() -> f64 {
113    2.0
114}
115
116fn default_max_restart_attempts() -> u32 {
117    10
118}
119
120impl Default for RestartPolicy {
121    fn default() -> Self {
122        Self {
123            initial_delay_ms: default_initial_delay(),
124            max_delay_ms: default_max_delay(),
125            backoff_multiplier: default_backoff_multiplier(),
126            max_restart_attempts: default_max_restart_attempts(),
127        }
128    }
129}
130
131/// Internal representation of Kiro's `mcp.json` file format.
132///
133/// The top-level JSON object contains a `mcpServers` key mapping server IDs
134/// to their configurations.
135#[derive(Deserialize)]
136#[serde(rename_all = "camelCase")]
137#[allow(dead_code)] // Used by McpServerManager in manager.rs (Task 3+)
138pub(crate) struct McpJsonFile {
139    /// Map of server ID to server configuration.
140    pub mcp_servers: HashMap<String, McpServerConfig>,
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn test_default_restart_policy() {
149        let policy = RestartPolicy::default();
150        assert_eq!(policy.initial_delay_ms, 1000);
151        assert_eq!(policy.max_delay_ms, 30000);
152        assert!((policy.backoff_multiplier - 2.0).abs() < f64::EPSILON);
153        assert_eq!(policy.max_restart_attempts, 10);
154    }
155
156    #[test]
157    fn test_mcp_server_config_defaults() {
158        let json = r#"{"command": "echo"}"#;
159        let config: McpServerConfig = serde_json::from_str(json).unwrap();
160        assert_eq!(config.command, "echo");
161        assert!(config.args.is_empty());
162        assert!(config.env.is_empty());
163        assert!(!config.disabled);
164        assert!(config.auto_approve.is_empty());
165        assert!(config.restart_policy.is_none());
166    }
167
168    #[test]
169    fn test_mcp_json_file_parsing() {
170        let json = r#"{
171            "mcpServers": {
172                "filesystem": {
173                    "command": "npx",
174                    "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
175                    "env": {},
176                    "disabled": false,
177                    "autoApprove": ["read_file", "list_directory"]
178                }
179            }
180        }"#;
181        let file: McpJsonFile = serde_json::from_str(json).unwrap();
182        assert_eq!(file.mcp_servers.len(), 1);
183        let fs_config = &file.mcp_servers["filesystem"];
184        assert_eq!(fs_config.command, "npx");
185        assert_eq!(fs_config.auto_approve, vec!["read_file", "list_directory"]);
186    }
187
188    #[test]
189    fn test_restart_policy_serde_defaults() {
190        let json = r#"{}"#;
191        let policy: RestartPolicy = serde_json::from_str(json).unwrap();
192        assert_eq!(policy.initial_delay_ms, 1000);
193        assert_eq!(policy.max_delay_ms, 30000);
194        assert!((policy.backoff_multiplier - 2.0).abs() < f64::EPSILON);
195        assert_eq!(policy.max_restart_attempts, 10);
196    }
197
198    #[test]
199    fn test_config_round_trip() {
200        let config = McpServerConfig {
201            command: "npx".to_string(),
202            args: vec!["-y".to_string(), "server".to_string()],
203            env: HashMap::from([("KEY".to_string(), "value".to_string())]),
204            disabled: true,
205            auto_approve: vec!["tool1".to_string()],
206            restart_policy: Some(RestartPolicy {
207                initial_delay_ms: 500,
208                max_delay_ms: 10000,
209                backoff_multiplier: 1.5,
210                max_restart_attempts: 3,
211            }),
212        };
213        let json = serde_json::to_string(&config).unwrap();
214        let deserialized: McpServerConfig = serde_json::from_str(&json).unwrap();
215        assert_eq!(config, deserialized);
216    }
217}