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}