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}