magi_code/config/settings/
services.rs1use 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}