use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use crate::ai_hooks::ConfigOrigin;
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct McpRegisterConfig {
pub agents: Vec<McpAgentTarget>,
pub scope: McpRegistrationScope,
pub allow_custom_servers: bool,
pub jarvy: Option<JarvyServerOverride>,
#[serde(rename = "server", default)]
pub servers: Vec<McpServerSpec>,
#[serde(default)]
pub library_sources: Vec<crate::library_registry::LibrarySource>,
#[serde(skip)]
pub origin: ConfigOrigin,
}
impl McpRegisterConfig {
pub fn is_empty(&self) -> bool {
self.agents.is_empty()
}
pub fn unique_agents(&self) -> Vec<McpAgentTarget> {
let set: BTreeSet<_> = self.agents.iter().copied().collect();
set.into_iter().collect()
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct JarvyServerOverride {
pub command: Option<String>,
pub args: Option<Vec<String>>,
pub env: std::collections::BTreeMap<String, String>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct McpServerSpec {
pub name: String,
pub transport: McpServerTransport,
pub command: Option<String>,
#[serde(default)]
pub args: Vec<String>,
pub url: Option<String>,
#[serde(default)]
pub env: std::collections::BTreeMap<String, String>,
#[serde(default)]
pub agents: Vec<McpAgentTarget>,
#[serde(rename = "use", default)]
pub use_library: Option<String>,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum McpServerTransport {
#[default]
Stdio,
Http,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum McpRegistrationScope {
#[default]
User,
Project,
}
pub use crate::agents::Agent as McpAgentTarget;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_minimal_config() {
let toml = r#"
agents = ["claude-code", "cursor"]
"#;
let cfg: McpRegisterConfig = toml::from_str(toml).unwrap();
assert_eq!(cfg.agents.len(), 2);
assert!(cfg.servers.is_empty());
assert!(!cfg.allow_custom_servers);
assert_eq!(cfg.origin, ConfigOrigin::Local);
}
#[test]
fn parses_jarvy_override_and_custom_server() {
let toml = r#"
agents = ["claude-code"]
allow_custom_servers = true
[jarvy]
command = "/usr/local/bin/jarvy"
args = ["mcp", "--verbose"]
[[server]]
name = "github"
transport = "stdio"
command = "gh-mcp-server"
"#;
let cfg: McpRegisterConfig = toml::from_str(toml).unwrap();
let jarvy = cfg.jarvy.expect("jarvy override");
assert_eq!(jarvy.command.as_deref(), Some("/usr/local/bin/jarvy"));
assert_eq!(jarvy.args.unwrap().len(), 2);
assert_eq!(cfg.servers[0].name, "github");
}
#[test]
fn rejects_unknown_fields() {
let toml = r#"
agents = ["cursor"]
mystery = true
"#;
assert!(toml::from_str::<McpRegisterConfig>(toml).is_err());
}
#[test]
fn agent_project_scope_support_matrix() {
assert!(McpAgentTarget::ClaudeCode.supports_project_scope());
assert!(McpAgentTarget::Cursor.supports_project_scope());
assert!(McpAgentTarget::Codex.supports_project_scope());
assert!(!McpAgentTarget::Windsurf.supports_project_scope());
assert!(!McpAgentTarget::Cline.supports_project_scope());
assert!(!McpAgentTarget::Continue.supports_project_scope());
}
}