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(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>,
}
#[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,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[repr(u8)]
pub enum McpAgentTarget {
ClaudeCode = 0,
Cursor = 1,
Codex = 2,
Windsurf = 3,
Cline = 4,
Continue = 5,
}
impl McpAgentTarget {
#[allow(dead_code)]
pub const ALL: &'static [McpAgentTarget] = &[
McpAgentTarget::ClaudeCode,
McpAgentTarget::Cursor,
McpAgentTarget::Codex,
McpAgentTarget::Windsurf,
McpAgentTarget::Cline,
McpAgentTarget::Continue,
];
#[allow(dead_code)]
pub const COUNT: usize = 6;
pub fn slug(self) -> &'static str {
match self {
McpAgentTarget::ClaudeCode => "claude-code",
McpAgentTarget::Cursor => "cursor",
McpAgentTarget::Codex => "codex",
McpAgentTarget::Windsurf => "windsurf",
McpAgentTarget::Cline => "cline",
McpAgentTarget::Continue => "continue",
}
}
#[allow(dead_code)]
pub fn supports_project_scope(self) -> bool {
matches!(
self,
McpAgentTarget::ClaudeCode | McpAgentTarget::Cursor | McpAgentTarget::Codex
)
}
}
impl std::fmt::Display for McpAgentTarget {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.slug())
}
}
#[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());
}
}