mcp_runner/config/
parser.rs

1use crate::error::{Error, Result};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::path::Path;
5
6/// Configuration for a single MCP server instance.
7///
8/// This structure defines how to start and configure a specific MCP server process.
9/// It includes the command to execute, any arguments to pass, and optional environment
10/// variables to set when launching the server.
11///
12/// # Examples
13///
14/// Basic server configuration:
15///
16/// ```
17/// use mcp_runner::config::ServerConfig;
18/// use std::collections::HashMap;
19///
20/// let server_config = ServerConfig {
21///     command: "node".to_string(),
22///     args: vec!["server.js".to_string()],
23///     env: HashMap::new(),
24/// };
25/// ```
26///
27/// Configuration with environment variables:
28///
29/// ```
30/// use mcp_runner::config::ServerConfig;
31/// use std::collections::HashMap;
32///
33/// let mut env = HashMap::new();
34/// env.insert("MODEL_PATH".to_string(), "/path/to/model".to_string());
35/// env.insert("DEBUG".to_string(), "true".to_string());
36///
37/// let server_config = ServerConfig {
38///     command: "python".to_string(),
39///     args: vec!["-m".to_string(), "mcp_server".to_string()],
40///     env,
41/// };
42/// ```
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct ServerConfig {
45    /// Command to execute when starting the MCP server.
46    /// This can be an absolute path or a command available in the PATH.
47    pub command: String,
48
49    /// Command-line arguments to pass to the server.
50    pub args: Vec<String>,
51
52    /// Environment variables to set when launching the server.
53    /// These will be combined with the current environment.
54    #[serde(default)]
55    pub env: HashMap<String, String>,
56}
57
58/// Main configuration for the MCP Runner.
59///
60/// This structure holds configurations for multiple MCP servers that can be
61/// managed by the runner. Each server has a unique name and its own configuration.
62///
63/// # JSON Schema
64///
65/// The configuration follows this JSON schema:
66///
67/// ```json
68/// {
69///   "mcpServers": {
70///     "server1": {
71///       "command": "node",
72///       "args": ["server.js"],
73///       "env": {
74///         "PORT": "3000",
75///         "DEBUG": "true"
76///       }
77///     },
78///     "server2": {
79///       "command": "python",
80///       "args": ["-m", "mcp_server"],
81///       "env": {}
82///     }
83///   }
84/// }
85/// ```
86///
87/// # Examples
88///
89/// Loading a configuration from a file:
90///
91/// ```no_run
92/// use mcp_runner::config::Config;
93///
94/// let config = Config::from_file("config.json").unwrap();
95/// ```
96///
97/// Accessing a server configuration:
98///
99/// ```
100/// use mcp_runner::config::{Config, ServerConfig};
101/// # use std::collections::HashMap;
102/// # let mut servers = HashMap::new();
103/// # let server_config = ServerConfig {
104/// #    command: "uvx".to_string(),
105/// #    args: vec!["mcp-server-fetch".to_string()],
106/// #    env: HashMap::new(),
107/// # };
108/// # servers.insert("fetch".to_string(), server_config);
109/// # let config = Config { mcp_servers: servers };
110///
111/// if let Some(server_config) = config.mcp_servers.get("fetch") {
112///     println!("Command: {}", server_config.command);
113/// }
114/// ```
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct Config {
117    /// Map of server names to their configurations.
118    /// The key is a unique identifier for each server.
119    #[serde(rename = "mcpServers")]
120    pub mcp_servers: HashMap<String, ServerConfig>,
121}
122
123impl Config {
124    /// Loads a configuration from a file path.
125    ///
126    /// This method reads the file at the specified path and parses its contents
127    /// as a JSON configuration.
128    ///
129    /// # Arguments
130    ///
131    /// * `path` - The path to the configuration file
132    ///
133    /// # Returns
134    ///
135    /// A `Result<Config>` that contains the parsed configuration or an error
136    ///
137    /// # Errors
138    ///
139    /// Returns an error if:
140    /// * The file cannot be read
141    /// * The file contents are not valid JSON
142    /// * The JSON does not conform to the expected schema
143    pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
144        let content = std::fs::read_to_string(path)
145            .map_err(|e| Error::ConfigParse(format!("Failed to read config file: {}", e)))?;
146
147        Self::parse_from_str(&content)
148    }
149
150    /// Parses a configuration from a JSON string.
151    ///
152    /// # Arguments
153    ///
154    /// * `content` - A string containing JSON configuration
155    ///
156    /// # Returns
157    ///
158    /// A `Result<Config>` that contains the parsed configuration or an error
159    ///
160    /// # Errors
161    ///
162    /// Returns an error if:
163    /// * The string is not valid JSON
164    /// * The JSON does not conform to the expected schema
165    pub fn parse_from_str(content: &str) -> Result<Self> {
166        serde_json::from_str(content)
167            .map_err(|e| Error::ConfigParse(format!("Failed to parse JSON config: {}", e)))
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn test_parse_claude_config() {
177        let config_str = r#"{
178            "mcpServers": {
179                "filesystem": {
180                    "command": "npx",
181                    "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed/files"]
182                }
183            }
184        }"#;
185
186        let config = Config::parse_from_str(config_str).unwrap();
187
188        assert_eq!(config.mcp_servers.len(), 1);
189        assert!(config.mcp_servers.contains_key("filesystem"));
190
191        let fs_config = &config.mcp_servers["filesystem"];
192        assert_eq!(fs_config.command, "npx");
193        assert_eq!(
194            fs_config.args,
195            vec![
196                "-y",
197                "@modelcontextprotocol/server-filesystem",
198                "/path/to/allowed/files"
199            ]
200        );
201    }
202}