Skip to main content

magi_code/config/settings/
services.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use std::collections::BTreeMap;
4
5use super::helpers::default_true;
6
7pub type McpServersSettings = BTreeMap<String, McpServerConfig>;
8pub type LspServersSettings = BTreeMap<String, LspServerConfig>;
9
10pub const DEFAULT_LSP_DIAGNOSTICS_WAIT_MS: u64 = 2_000;
11pub const DEFAULT_LSP_IDLE_SHUTDOWN_MINUTES: u64 = 10;
12pub const MAX_LSP_DIAGNOSTICS_WAIT_MS: u64 = 30_000;
13pub const MAX_LSP_IDLE_SHUTDOWN_MINUTES: u64 = 240;
14
15#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
16pub struct LspSettings {
17    #[serde(default)]
18    pub enabled: bool,
19    #[serde(default = "default_true")]
20    pub inject_diagnostics_on_edit: bool,
21    #[serde(default = "default_lsp_diagnostics_wait_ms")]
22    pub diagnostics_wait_ms: u64,
23    #[serde(default = "default_lsp_idle_shutdown_minutes")]
24    pub idle_shutdown_minutes: u64,
25    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
26    pub servers: LspServersSettings,
27}
28
29impl Default for LspSettings {
30    fn default() -> Self {
31        Self {
32            enabled: false,
33            inject_diagnostics_on_edit: true,
34            diagnostics_wait_ms: default_lsp_diagnostics_wait_ms(),
35            idle_shutdown_minutes: default_lsp_idle_shutdown_minutes(),
36            servers: BTreeMap::new(),
37        }
38    }
39}
40
41impl LspSettings {
42    pub(crate) fn is_default(&self) -> bool {
43        self == &Self::default()
44    }
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
48pub struct LspServerConfig {
49    pub command: String,
50    #[serde(default, skip_serializing_if = "Vec::is_empty")]
51    pub args: Vec<String>,
52    #[serde(default = "default_true")]
53    pub enabled: bool,
54}
55
56pub(crate) fn default_lsp_diagnostics_wait_ms() -> u64 {
57    DEFAULT_LSP_DIAGNOSTICS_WAIT_MS
58}
59
60pub(crate) fn default_lsp_idle_shutdown_minutes() -> u64 {
61    DEFAULT_LSP_IDLE_SHUTDOWN_MINUTES
62}
63
64pub const DEFAULT_MCP_TIMEOUT_SECONDS: u64 = 30;
65pub const MAX_MCP_TIMEOUT_SECONDS: u64 = 300;
66
67#[derive(Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
68#[serde(tag = "type", rename_all = "snake_case")]
69pub enum McpServerConfig {
70    Stdio(McpStdioServerConfig),
71    Http(McpHttpServerConfig),
72}
73
74impl std::fmt::Debug for McpServerConfig {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        match self {
77            Self::Stdio(config) => f.debug_tuple("Stdio").field(config).finish(),
78            Self::Http(config) => f.debug_tuple("Http").field(config).finish(),
79        }
80    }
81}
82
83impl McpServerConfig {
84    pub(crate) fn enabled(&self) -> bool {
85        match self {
86            Self::Stdio(config) => config.enabled,
87            Self::Http(config) => config.enabled,
88        }
89    }
90
91    pub(crate) fn set_enabled(&mut self, enabled: bool) {
92        match self {
93            Self::Stdio(config) => config.enabled = enabled,
94            Self::Http(config) => config.enabled = enabled,
95        }
96    }
97}
98
99#[derive(Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
100pub struct McpStdioServerConfig {
101    pub command: String,
102    #[serde(default, skip_serializing_if = "Vec::is_empty")]
103    pub args: Vec<String>,
104    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
105    pub env: BTreeMap<String, String>,
106    #[serde(default = "default_true")]
107    pub enabled: bool,
108    #[serde(default, skip_serializing_if = "Option::is_none")]
109    pub timeout: Option<u64>,
110}
111
112impl std::fmt::Debug for McpStdioServerConfig {
113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114        f.debug_struct("McpStdioServerConfig")
115            .field("command", &self.command)
116            .field("args", &self.args)
117            .field("env", &format_args!("<{} vars redacted>", self.env.len()))
118            .field("enabled", &self.enabled)
119            .field("timeout", &self.timeout)
120            .finish()
121    }
122}
123
124#[derive(Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
125pub struct McpHttpServerConfig {
126    pub url: String,
127    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
128    pub headers: BTreeMap<String, String>,
129    #[serde(default, skip_serializing_if = "Option::is_none")]
130    pub oauth: Option<McpOAuthConfig>,
131    #[serde(default = "default_true")]
132    pub enabled: bool,
133    #[serde(default, skip_serializing_if = "Option::is_none")]
134    pub timeout: Option<u64>,
135}
136
137#[derive(Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
138pub struct McpOAuthConfig {
139    #[serde(default, skip_serializing_if = "Option::is_none")]
140    pub client_id: Option<String>,
141    #[serde(default, skip_serializing_if = "Vec::is_empty")]
142    pub scopes: Vec<String>,
143    #[serde(default, skip_serializing_if = "Option::is_none")]
144    pub authorization_server: Option<String>,
145}
146
147impl std::fmt::Debug for McpOAuthConfig {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        f.debug_struct("McpOAuthConfig")
150            .field("client_id", &self.client_id.as_ref().map(|_| "[REDACTED]"))
151            .field("scopes", &self.scopes)
152            .field(
153                "authorization_server",
154                &self
155                    .authorization_server
156                    .as_ref()
157                    .map(|url| sanitize_mcp_http_url_for_display(url)),
158            )
159            .finish()
160    }
161}
162
163impl std::fmt::Debug for McpHttpServerConfig {
164    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165        f.debug_struct("McpHttpServerConfig")
166            .field("url", &sanitize_mcp_http_url_for_display(&self.url))
167            .field(
168                "headers",
169                &crate::mcp::headers::redact_headers(&self.headers),
170            )
171            .field("oauth", &self.oauth)
172            .field("enabled", &self.enabled)
173            .field("timeout", &self.timeout)
174            .finish()
175    }
176}
177
178fn sanitize_mcp_http_url_for_display(url: &str) -> String {
179    match reqwest::Url::parse(url) {
180        Ok(parsed) => {
181            let host = parsed.host_str().unwrap_or("<unknown>");
182            let port = parsed
183                .port()
184                .map(|port| format!(":{port}"))
185                .unwrap_or_default();
186            format!("{}://{}{}{}", parsed.scheme(), host, port, parsed.path())
187        }
188        Err(_) => "<invalid-url>".to_string(),
189    }
190}