use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use super::helpers::default_true;
pub type McpServersSettings = BTreeMap<String, McpServerConfig>;
pub type LspServersSettings = BTreeMap<String, LspServerConfig>;
pub const DEFAULT_LSP_DIAGNOSTICS_WAIT_MS: u64 = 2_000;
pub const DEFAULT_LSP_IDLE_SHUTDOWN_MINUTES: u64 = 10;
pub const MAX_LSP_DIAGNOSTICS_WAIT_MS: u64 = 30_000;
pub const MAX_LSP_IDLE_SHUTDOWN_MINUTES: u64 = 240;
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct LspSettings {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_true")]
pub inject_diagnostics_on_edit: bool,
#[serde(default = "default_lsp_diagnostics_wait_ms")]
pub diagnostics_wait_ms: u64,
#[serde(default = "default_lsp_idle_shutdown_minutes")]
pub idle_shutdown_minutes: u64,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub servers: LspServersSettings,
}
impl Default for LspSettings {
fn default() -> Self {
Self {
enabled: false,
inject_diagnostics_on_edit: true,
diagnostics_wait_ms: default_lsp_diagnostics_wait_ms(),
idle_shutdown_minutes: default_lsp_idle_shutdown_minutes(),
servers: BTreeMap::new(),
}
}
}
impl LspSettings {
pub(crate) fn is_default(&self) -> bool {
self == &Self::default()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct LspServerConfig {
pub command: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub args: Vec<String>,
#[serde(default = "default_true")]
pub enabled: bool,
}
pub(crate) fn default_lsp_diagnostics_wait_ms() -> u64 {
DEFAULT_LSP_DIAGNOSTICS_WAIT_MS
}
pub(crate) fn default_lsp_idle_shutdown_minutes() -> u64 {
DEFAULT_LSP_IDLE_SHUTDOWN_MINUTES
}
pub const DEFAULT_MCP_TIMEOUT_SECONDS: u64 = 30;
pub const MAX_MCP_TIMEOUT_SECONDS: u64 = 300;
#[derive(Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum McpServerConfig {
Stdio(McpStdioServerConfig),
Http(McpHttpServerConfig),
}
impl std::fmt::Debug for McpServerConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Stdio(config) => f.debug_tuple("Stdio").field(config).finish(),
Self::Http(config) => f.debug_tuple("Http").field(config).finish(),
}
}
}
impl McpServerConfig {
pub(crate) fn enabled(&self) -> bool {
match self {
Self::Stdio(config) => config.enabled,
Self::Http(config) => config.enabled,
}
}
pub(crate) fn set_enabled(&mut self, enabled: bool) {
match self {
Self::Stdio(config) => config.enabled = enabled,
Self::Http(config) => config.enabled = enabled,
}
}
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct McpStdioServerConfig {
pub command: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub args: Vec<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub env: BTreeMap<String, String>,
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timeout: Option<u64>,
}
impl std::fmt::Debug for McpStdioServerConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("McpStdioServerConfig")
.field("command", &self.command)
.field("args", &self.args)
.field("env", &format_args!("<{} vars redacted>", self.env.len()))
.field("enabled", &self.enabled)
.field("timeout", &self.timeout)
.finish()
}
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct McpHttpServerConfig {
pub url: String,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub headers: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub oauth: Option<McpOAuthConfig>,
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timeout: Option<u64>,
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
pub struct McpOAuthConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client_id: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub scopes: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub authorization_server: Option<String>,
}
impl std::fmt::Debug for McpOAuthConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("McpOAuthConfig")
.field("client_id", &self.client_id.as_ref().map(|_| "[REDACTED]"))
.field("scopes", &self.scopes)
.field(
"authorization_server",
&self
.authorization_server
.as_ref()
.map(|url| sanitize_mcp_http_url_for_display(url)),
)
.finish()
}
}
impl std::fmt::Debug for McpHttpServerConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("McpHttpServerConfig")
.field("url", &sanitize_mcp_http_url_for_display(&self.url))
.field(
"headers",
&crate::mcp::headers::redact_headers(&self.headers),
)
.field("oauth", &self.oauth)
.field("enabled", &self.enabled)
.field("timeout", &self.timeout)
.finish()
}
}
fn sanitize_mcp_http_url_for_display(url: &str) -> String {
match reqwest::Url::parse(url) {
Ok(parsed) => {
let host = parsed.host_str().unwrap_or("<unknown>");
let port = parsed
.port()
.map(|port| format!(":{port}"))
.unwrap_or_default();
format!("{}://{}{}{}", parsed.scheme(), host, port, parsed.path())
}
Err(_) => "<invalid-url>".to_string(),
}
}