mcp_runner/config/parser.rs
1use crate::error::{Error, Result};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::path::Path;
5
6/// Constants for default configuration values
7/// Default address for the SSE proxy server (localhost)
8pub const DEFAULT_ADDRESS: &str = "127.0.0.1";
9/// Default port for the SSE proxy server
10pub const DEFAULT_PORT: u16 = 3000;
11/// Default number of worker threads for the Actix Web server
12pub const DEFAULT_WORKERS: usize = 4;
13
14/// Configuration for a single MCP server instance.
15///
16/// This structure defines how to start and configure a specific MCP server process.
17/// It includes the command to execute, any arguments to pass, and optional environment
18/// variables to set when launching the server.
19///
20/// # Examples
21///
22/// Basic server configuration:
23///
24/// ```
25/// use mcp_runner::config::ServerConfig;
26/// use std::collections::HashMap;
27///
28/// let server_config = ServerConfig {
29/// command: "node".to_string(),
30/// args: vec!["server.js".to_string()],
31/// env: HashMap::new(),
32/// };
33/// ```
34///
35/// Configuration with environment variables:
36///
37/// ```
38/// use mcp_runner::config::ServerConfig;
39/// use std::collections::HashMap;
40///
41/// let mut env = HashMap::new();
42/// env.insert("MODEL_PATH".to_string(), "/path/to/model".to_string());
43/// env.insert("DEBUG".to_string(), "true".to_string());
44///
45/// let server_config = ServerConfig {
46/// command: "python".to_string(),
47/// args: vec!["-m".to_string(), "mcp_server".to_string()],
48/// env,
49/// };
50/// ```
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct ServerConfig {
53 /// Command to execute when starting the MCP server.
54 /// This can be an absolute path or a command available in the PATH.
55 pub command: String,
56
57 /// Command-line arguments to pass to the server.
58 pub args: Vec<String>,
59
60 /// Environment variables to set when launching the server.
61 /// These will be combined with the current environment.
62 #[serde(default)]
63 pub env: HashMap<String, String>,
64}
65
66/// Authentication configuration for SSE Proxy
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct AuthConfig {
69 /// Bearer authentication configuration
70 #[serde(default)]
71 pub bearer: Option<BearerAuthConfig>,
72}
73
74/// Bearer token authentication configuration
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct BearerAuthConfig {
77 /// Authentication token
78 pub token: String,
79}
80
81/// Server-Sent Events (SSE) Proxy configuration
82///
83/// This structure defines the configuration for the SSE proxy server, which allows
84/// web clients to connect to MCP servers via HTTP and receive real-time updates
85/// through Server-Sent Events. The proxy provides authentication, server access control,
86/// and network binding options.
87///
88/// # Examples
89///
90/// Basic SSE proxy configuration with default address and port:
91///
92/// ```
93/// use mcp_runner::config::SSEProxyConfig;
94///
95/// let proxy_config = SSEProxyConfig {
96/// allowed_servers: None, // Allow all servers
97/// authenticate: None, // No authentication required
98/// address: "127.0.0.1".to_string(),
99/// port: 3000,
100/// workers: None,
101/// };
102/// ```
103///
104/// Secure SSE proxy configuration with restrictions:
105///
106/// ```
107/// use mcp_runner::config::{SSEProxyConfig, AuthConfig, BearerAuthConfig};
108///
109/// let auth_config = AuthConfig {
110/// bearer: Some(BearerAuthConfig {
111/// token: "secure_token_string".to_string(),
112/// }),
113/// };
114///
115/// let proxy_config = SSEProxyConfig {
116/// allowed_servers: Some(vec!["fetch-server".to_string(), "embedding-server".to_string()]),
117/// authenticate: Some(auth_config),
118/// address: "0.0.0.0".to_string(), // Listen on all interfaces
119/// port: 8080,
120/// workers: Some(4),
121/// };
122/// ```
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct SSEProxyConfig {
125 /// List of allowed server names that clients can access
126 ///
127 /// When specified, only servers in this list can be accessed through the proxy.
128 /// If `None`, all servers defined in the configuration are accessible.
129 #[serde(default, rename = "allowedServers")]
130 pub allowed_servers: Option<Vec<String>>,
131
132 /// Authentication configuration for securing the proxy
133 ///
134 /// When specified, clients must provide valid authentication credentials.
135 /// If `None`, the proxy accepts all connections without authentication.
136 #[serde(default)]
137 pub authenticate: Option<AuthConfig>,
138
139 /// Network address the proxy server will bind to
140 ///
141 /// Use "127.0.0.1" to allow only local connections, or "0.0.0.0" to accept
142 /// connections from any network interface.
143 #[serde(default = "default_address")]
144 pub address: String,
145
146 /// TCP port the proxy server will listen on
147 ///
148 /// The port must be available and not require elevated privileges (typically
149 /// ports above 1024 unless running with administrator/root privileges).
150 #[serde(default = "default_port")]
151 pub port: u16,
152
153 /// Number of worker threads for the Actix Web server
154 ///
155 /// When specified, Actix Web will use this number of workers.
156 /// If `None`, the default value of 4 workers will be used.
157 #[serde(default)]
158 pub workers: Option<usize>,
159}
160
161/// Default address for the SSE proxy
162fn default_address() -> String {
163 DEFAULT_ADDRESS.to_string()
164}
165
166/// Default port for the SSE proxy
167fn default_port() -> u16 {
168 DEFAULT_PORT
169}
170
171impl Default for SSEProxyConfig {
172 fn default() -> Self {
173 Self {
174 allowed_servers: None,
175 authenticate: None,
176 address: default_address(),
177 port: default_port(),
178 workers: None,
179 }
180 }
181}
182
183/// Main configuration for the MCP Runner.
184///
185/// This structure holds configurations for multiple MCP servers that can be
186/// managed by the runner. Each server has a unique name and its own configuration.
187///
188/// # JSON Schema
189///
190/// The configuration follows this JSON schema:
191///
192/// ```json
193/// {
194/// "mcpServers": {
195/// "server1": {
196/// "command": "node",
197/// "args": ["server.js"],
198/// "env": {
199/// "PORT": "3000",
200/// "DEBUG": "true"
201/// }
202/// },
203/// "server2": {
204/// "command": "python",
205/// "args": ["-m", "mcp_server"],
206/// "env": {}
207/// }
208/// },
209/// "sseProxy": {
210/// "allowedServers": ["server1"],
211/// "authenticate": {
212/// "bearer": {
213/// "token": "your_token"
214/// }
215/// },
216/// "address": "127.0.0.1",
217/// "port": 3000,
218/// "workers": 4
219/// }
220/// }
221/// ```
222///
223/// # Examples
224///
225/// Loading a configuration from a file:
226///
227/// ```no_run
228/// use mcp_runner::config::Config;
229///
230/// let config = Config::from_file("config.json").unwrap();
231/// ```
232///
233/// Accessing a server configuration:
234///
235/// ```
236/// use mcp_runner::config::{Config, ServerConfig};
237/// # use std::collections::HashMap;
238/// # let mut servers = HashMap::new();
239/// # let server_config = ServerConfig {
240/// # command: "uvx".to_string(),
241/// # args: vec!["mcp-server-fetch".to_string()],
242/// # env: HashMap::new(),
243/// # };
244/// # servers.insert("fetch".to_string(), server_config);
245/// # let config = Config { mcp_servers: servers, sse_proxy: None };
246///
247/// if let Some(server_config) = config.mcp_servers.get("fetch") {
248/// println!("Command: {}", server_config.command);
249/// }
250/// ```
251#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct Config {
253 /// Map of server names to their configurations.
254 /// The key is a unique identifier for each server.
255 #[serde(rename = "mcpServers")]
256 pub mcp_servers: HashMap<String, ServerConfig>,
257
258 /// SSE Proxy configuration, if None the proxy is disabled
259 #[serde(rename = "sseProxy", default)]
260 pub sse_proxy: Option<SSEProxyConfig>,
261}
262
263impl Config {
264 /// Loads a configuration from a file path.
265 ///
266 /// This method reads the file at the specified path and parses its contents
267 /// as a JSON configuration.
268 ///
269 /// # Arguments
270 ///
271 /// * `path` - The path to the configuration file
272 ///
273 /// # Returns
274 ///
275 /// A `Result<Config>` that contains the parsed configuration or an error
276 ///
277 /// # Errors
278 ///
279 /// Returns an error if:
280 /// * The file cannot be read
281 /// * The file contents are not valid JSON
282 /// * The JSON does not conform to the expected schema
283 pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
284 let content = std::fs::read_to_string(path)
285 .map_err(|e| Error::ConfigParse(format!("Failed to read config file: {}", e)))?;
286
287 Self::parse_from_str(&content)
288 }
289
290 /// Parses a configuration from a JSON string.
291 ///
292 /// # Arguments
293 ///
294 /// * `content` - A string containing JSON configuration
295 ///
296 /// # Returns
297 ///
298 /// A `Result<Config>` that contains the parsed configuration or an error
299 ///
300 /// # Errors
301 ///
302 /// Returns an error if:
303 /// * The string is not valid JSON
304 /// * The JSON does not conform to the expected schema
305 pub fn parse_from_str(content: &str) -> Result<Self> {
306 serde_json::from_str(content)
307 .map_err(|e| Error::ConfigParse(format!("Failed to parse JSON config: {}", e)))
308 }
309}