use std::path::PathBuf;
#[derive(Debug, Clone)]
pub enum Transport {
Remote {
url: String,
key: String,
},
Stdio {
db_path: String,
},
OauthRemote {
url: String,
},
}
#[derive(Debug, Clone)]
pub struct ServerConfig {
pub name: String,
pub transport: Transport,
}
impl ServerConfig {
pub fn remote(url: impl Into<String>, key: impl Into<String>) -> Self {
Self {
name: "lific".into(),
transport: Transport::Remote {
url: url.into(),
key: key.into(),
},
}
}
pub fn stdio(db_path: impl Into<String>) -> Self {
Self {
name: "lific".into(),
transport: Transport::Stdio {
db_path: db_path.into(),
},
}
}
pub fn oauth_remote(url: impl Into<String>) -> Self {
Self {
name: "lific".into(),
transport: Transport::OauthRemote { url: url.into() },
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OauthSupport {
Capable { hint: &'static str },
Unsupported { reason: &'static str },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Format {
Json,
Toml,
Yaml,
}
impl Format {
pub fn as_str(self) -> &'static str {
match self {
Format::Json => "json",
Format::Toml => "toml",
Format::Yaml => "yaml",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Scope {
Global,
Project,
}
impl Scope {
pub fn as_str(self) -> &'static str {
match self {
Scope::Global => "global",
Scope::Project => "project",
}
}
}
#[derive(Debug, Clone)]
pub struct PathBase {
pub home: PathBuf,
pub project: PathBuf,
pub os: Os,
pub appdata: Option<PathBuf>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Os {
Linux,
Mac,
Windows,
}
impl Os {
pub fn host() -> Os {
match std::env::consts::OS {
"macos" => Os::Mac,
"windows" => Os::Windows,
_ => Os::Linux,
}
}
}
#[derive(Default)]
pub struct CompiledEntry {
pub name: String,
pub top_key: String,
pub value: serde_json::Value,
pub notes: Vec<String>,
}
pub struct ClientSpec {
pub id: &'static str,
pub display: &'static str,
pub oauth: OauthSupport,
pub format: Format,
global_path: fn(&PathBase) -> Option<PathBuf>,
project_path: fn(&PathBase) -> Option<PathBuf>,
compile: fn(&ServerConfig) -> CompiledEntry,
detect_extra: fn(&PathBase, Scope) -> Vec<PathBuf>,
}
impl ClientSpec {
pub fn path_for(&self, base: &PathBase, scope: Scope) -> Option<PathBuf> {
match scope {
Scope::Global => (self.global_path)(base),
Scope::Project => (self.project_path)(base),
}
}
pub fn compile(&self, cfg: &ServerConfig) -> CompiledEntry {
let mut entry = (self.compile)(cfg);
entry.name = cfg.name.clone();
entry
}
pub fn detected(&self, base: &PathBase, scope: Scope) -> bool {
if let Some(p) = self.path_for(base, scope)
&& p.exists()
{
return true;
}
(self.detect_extra)(base, scope).iter().any(|p| p.exists())
}
}
fn config_dir(base: &PathBase, rest: &[&str]) -> PathBuf {
match base.os {
Os::Windows => {
let mut p = base
.appdata
.clone()
.unwrap_or_else(|| base.home.join("AppData").join("Roaming"));
for r in rest {
p = p.join(r);
}
p
}
_ => {
let mut p = base.home.join(".config");
for r in rest {
p = p.join(r);
}
p
}
}
}
fn home_dot(base: &PathBase, rest: &[&str]) -> PathBuf {
let mut p = base.home.clone();
for r in rest {
p = p.join(r);
}
p
}
fn project_rel(base: &PathBase, rest: &[&str]) -> PathBuf {
let mut p = base.project.clone();
for r in rest {
p = p.join(r);
}
p
}
fn no_extra(_: &PathBase, _: Scope) -> Vec<PathBuf> {
Vec::new()
}
fn bearer(key: &str) -> String {
format!("Bearer {key}")
}
fn headers_obj(key: &str) -> serde_json::Value {
serde_json::json!({ "Authorization": bearer(key) })
}
fn stdio_args(db_path: &str) -> serde_json::Value {
serde_json::json!(["--db", db_path, "mcp"])
}
pub fn all_clients() -> Vec<ClientSpec> {
vec![
ClientSpec {
id: "opencode",
display: "OpenCode",
oauth: OauthSupport::Capable {
hint: "opencode mcp auth lific",
},
format: Format::Json,
global_path: |b| Some(config_dir(b, &["opencode", "opencode.json"])),
project_path: |b| Some(project_rel(b, &["opencode.json"])),
detect_extra: no_extra,
compile: |c| match &c.transport {
Transport::Remote { url, key } => CompiledEntry {
name: String::new(),
top_key: "mcp".into(),
value: serde_json::json!({
"type": "remote",
"url": url,
"headers": headers_obj(key),
"enabled": true,
}),
notes: vec![],
},
Transport::OauthRemote { url } => CompiledEntry {
name: String::new(),
top_key: "mcp".into(),
value: serde_json::json!({
"type": "remote",
"url": url,
"enabled": true,
}),
notes: vec![],
},
Transport::Stdio { db_path } => CompiledEntry {
name: String::new(),
top_key: "mcp".into(),
value: serde_json::json!({
"type": "local",
"command": ["lific", "--db", db_path, "mcp"],
"enabled": true,
}),
notes: vec![],
},
},
},
ClientSpec {
id: "claude-code",
display: "Claude Code",
oauth: OauthSupport::Capable {
hint: "claude mcp login lific (or /mcp inside a session)",
},
format: Format::Json,
global_path: |b| Some(home_dot(b, &[".claude.json"])),
project_path: |b| Some(project_rel(b, &[".mcp.json"])),
detect_extra: no_extra,
compile: |c| match &c.transport {
Transport::Remote { url, key } => CompiledEntry {
name: String::new(),
top_key: "mcpServers".into(),
value: serde_json::json!({
"type": "http",
"url": url,
"headers": headers_obj(key),
}),
notes: vec![],
},
Transport::OauthRemote { url } => CompiledEntry {
name: String::new(),
top_key: "mcpServers".into(),
value: serde_json::json!({
"type": "http",
"url": url,
}),
notes: vec![],
},
Transport::Stdio { db_path } => CompiledEntry {
name: String::new(),
top_key: "mcpServers".into(),
value: serde_json::json!({
"type": "stdio",
"command": "lific",
"args": stdio_args(db_path),
}),
notes: vec![],
},
},
},
ClientSpec {
id: "claude-desktop",
display: "Claude Desktop",
oauth: OauthSupport::Unsupported {
reason: "Claude Desktop's OAuth path is the Settings > Connectors UI, not a \
config-file entry — add Lific there instead.",
},
format: Format::Json,
global_path: |b| {
Some(match b.os {
Os::Mac => home_dot(
b,
&[
"Library",
"Application Support",
"Claude",
"claude_desktop_config.json",
],
),
Os::Windows => {
config_dir(b, &["Claude", "claude_desktop_config.json"])
}
Os::Linux => config_dir(b, &["Claude", "claude_desktop_config.json"]),
})
},
project_path: |_| None,
detect_extra: no_extra,
compile: |c| match &c.transport {
Transport::Remote { url, key } => CompiledEntry {
name: String::new(),
top_key: "mcpServers".into(),
value: serde_json::json!({
"command": "npx",
"args": [
"-y",
"mcp-remote",
url,
"--header",
format!("Authorization: {}", bearer(key)),
],
}),
notes: vec![
"Claude Desktop reaches the remote server via the mcp-remote npx shim; \
npx installs it on first run."
.into(),
],
},
Transport::OauthRemote { url } => CompiledEntry {
name: String::new(),
top_key: "mcpServers".into(),
value: serde_json::json!({
"command": "npx",
"args": ["-y", "mcp-remote", url],
}),
notes: vec![],
},
Transport::Stdio { db_path } => CompiledEntry {
name: String::new(),
top_key: "mcpServers".into(),
value: serde_json::json!({
"command": "lific",
"args": stdio_args(db_path),
}),
notes: vec![],
},
},
},
ClientSpec {
id: "cursor",
display: "Cursor",
oauth: OauthSupport::Capable {
hint: "Cursor will prompt to authorize in-app on first connect",
},
format: Format::Json,
global_path: |b| Some(home_dot(b, &[".cursor", "mcp.json"])),
project_path: |b| Some(project_rel(b, &[".cursor", "mcp.json"])),
detect_extra: |b, scope| match scope {
Scope::Global => vec![home_dot(b, &[".cursor"])],
Scope::Project => vec![project_rel(b, &[".cursor"])],
},
compile: |c| match &c.transport {
Transport::Remote { url, key } => CompiledEntry {
name: String::new(),
top_key: "mcpServers".into(),
value: serde_json::json!({
"url": url,
"headers": headers_obj(key),
}),
notes: vec![],
},
Transport::OauthRemote { url } => CompiledEntry {
name: String::new(),
top_key: "mcpServers".into(),
value: serde_json::json!({
"url": url,
}),
notes: vec![],
},
Transport::Stdio { db_path } => CompiledEntry {
name: String::new(),
top_key: "mcpServers".into(),
value: serde_json::json!({
"command": "lific",
"args": stdio_args(db_path),
}),
notes: vec![],
},
},
},
ClientSpec {
id: "vscode",
display: "VS Code",
oauth: OauthSupport::Capable {
hint: "VS Code starts the browser OAuth flow on first connect",
},
format: Format::Json,
global_path: |b| {
Some(match b.os {
Os::Mac => home_dot(
b,
&["Library", "Application Support", "Code", "User", "mcp.json"],
),
Os::Windows => config_dir(b, &["Code", "User", "mcp.json"]),
Os::Linux => config_dir(b, &["Code", "User", "mcp.json"]),
})
},
project_path: |b| Some(project_rel(b, &[".vscode", "mcp.json"])),
detect_extra: |b, scope| match scope {
Scope::Project => vec![project_rel(b, &[".vscode"])],
Scope::Global => vec![],
},
compile: |c| match &c.transport {
Transport::Remote { url, key } => CompiledEntry {
name: String::new(),
top_key: "servers".into(),
value: serde_json::json!({
"type": "http",
"url": url,
"headers": headers_obj(key),
}),
notes: vec![],
},
Transport::OauthRemote { url } => CompiledEntry {
name: String::new(),
top_key: "servers".into(),
value: serde_json::json!({
"type": "http",
"url": url,
}),
notes: vec![],
},
Transport::Stdio { db_path } => CompiledEntry {
name: String::new(),
top_key: "servers".into(),
value: serde_json::json!({
"type": "stdio",
"command": "lific",
"args": stdio_args(db_path),
}),
notes: vec![],
},
},
},
ClientSpec {
id: "codex",
display: "Codex",
oauth: OauthSupport::Capable {
hint: "codex mcp login lific",
},
format: Format::Toml,
global_path: |b| Some(home_dot(b, &[".codex", "config.toml"])),
project_path: |b| Some(project_rel(b, &[".codex", "config.toml"])),
detect_extra: |b, scope| match scope {
Scope::Global => vec![home_dot(b, &[".codex"])],
Scope::Project => vec![project_rel(b, &[".codex"])],
},
compile: |c| match &c.transport {
Transport::Remote { url, .. } => CompiledEntry {
name: String::new(),
top_key: "mcp_servers.lific".into(),
value: serde_json::json!({
"url": url,
"bearer_token_env_var": "LIFIC_API_KEY",
}),
notes: vec![
"Codex reads the key from the LIFIC_API_KEY environment variable — \
export it before launching Codex (see the hint below)."
.into(),
],
},
Transport::OauthRemote { url } => CompiledEntry {
name: String::new(),
top_key: "mcp_servers.lific".into(),
value: serde_json::json!({
"url": url,
}),
notes: vec![],
},
Transport::Stdio { db_path } => CompiledEntry {
name: String::new(),
top_key: "mcp_servers.lific".into(),
value: serde_json::json!({
"command": "lific",
"args": stdio_args(db_path),
}),
notes: vec![],
},
},
},
ClientSpec {
id: "zed",
display: "Zed",
oauth: OauthSupport::Capable {
hint: "Zed runs the OAuth flow automatically when no header is set",
},
format: Format::Json,
global_path: |b| Some(config_dir(b, &["zed", "settings.json"])),
project_path: |_| None,
detect_extra: |b, scope| match scope {
Scope::Global => vec![config_dir(b, &["zed"])],
Scope::Project => vec![],
},
compile: |c| match &c.transport {
Transport::Remote { url, key } => CompiledEntry {
name: String::new(),
top_key: "context_servers".into(),
value: serde_json::json!({
"url": url,
"headers": headers_obj(key),
}),
notes: vec![],
},
Transport::OauthRemote { url } => CompiledEntry {
name: String::new(),
top_key: "context_servers".into(),
value: serde_json::json!({
"url": url,
}),
notes: vec![],
},
Transport::Stdio { db_path } => CompiledEntry {
name: String::new(),
top_key: "context_servers".into(),
value: serde_json::json!({
"command": "lific",
"args": stdio_args(db_path),
}),
notes: vec![],
},
},
},
ClientSpec {
id: "gemini",
display: "Gemini CLI",
oauth: OauthSupport::Capable {
hint: "run /mcp auth lific inside Gemini CLI",
},
format: Format::Json,
global_path: |b| Some(home_dot(b, &[".gemini", "settings.json"])),
project_path: |b| Some(project_rel(b, &[".gemini", "settings.json"])),
detect_extra: |b, scope| match scope {
Scope::Global => vec![home_dot(b, &[".gemini"])],
Scope::Project => vec![project_rel(b, &[".gemini"])],
},
compile: |c| match &c.transport {
Transport::Remote { url, key } => CompiledEntry {
name: String::new(),
top_key: "mcpServers".into(),
value: serde_json::json!({
"httpUrl": url,
"headers": headers_obj(key),
}),
notes: vec![],
},
Transport::OauthRemote { url } => CompiledEntry {
name: String::new(),
top_key: "mcpServers".into(),
value: serde_json::json!({
"httpUrl": url,
}),
notes: vec![],
},
Transport::Stdio { db_path } => CompiledEntry {
name: String::new(),
top_key: "mcpServers".into(),
value: serde_json::json!({
"command": "lific",
"args": stdio_args(db_path),
}),
notes: vec![],
},
},
},
ClientSpec {
id: "windsurf",
display: "Windsurf",
oauth: OauthSupport::Capable {
hint: "Windsurf prompts to authorize in-app on first connect",
},
format: Format::Json,
global_path: |b| Some(home_dot(b, &[".codeium", "windsurf", "mcp_config.json"])),
project_path: |_| None,
detect_extra: |b, scope| match scope {
Scope::Global => vec![home_dot(b, &[".codeium", "windsurf"])],
Scope::Project => vec![],
},
compile: |c| match &c.transport {
Transport::Remote { url, key } => CompiledEntry {
name: String::new(),
top_key: "mcpServers".into(),
value: serde_json::json!({
"serverUrl": url,
"headers": headers_obj(key),
}),
notes: vec![],
},
Transport::OauthRemote { url } => CompiledEntry {
name: String::new(),
top_key: "mcpServers".into(),
value: serde_json::json!({
"serverUrl": url,
}),
notes: vec![],
},
Transport::Stdio { db_path } => CompiledEntry {
name: String::new(),
top_key: "mcpServers".into(),
value: serde_json::json!({
"command": "lific",
"args": stdio_args(db_path),
}),
notes: vec![],
},
},
},
ClientSpec {
id: "goose",
display: "Goose",
oauth: OauthSupport::Unsupported {
reason: "Goose's MCP OAuth support is unconfirmed — use the default key mode \
(drop --oauth) to connect it.",
},
format: Format::Yaml,
global_path: |b| Some(config_dir(b, &["goose", "config.yaml"])),
project_path: |_| None,
detect_extra: |b, scope| match scope {
Scope::Global => vec![config_dir(b, &["goose"])],
Scope::Project => vec![],
},
compile: |c| match &c.transport {
Transport::Remote { url, key } => CompiledEntry {
name: String::new(),
top_key: "extensions".into(),
value: serde_json::json!({
"name": "lific",
"type": "streamable_http",
"uri": url,
"headers": headers_obj(key),
"enabled": true,
"timeout": 300,
}),
notes: vec![
"Goose config is YAML; existing comments in config.yaml are not preserved \
across this edit."
.into(),
],
},
Transport::OauthRemote { url } => CompiledEntry {
name: String::new(),
top_key: "extensions".into(),
value: serde_json::json!({
"name": "lific",
"type": "streamable_http",
"uri": url,
"enabled": true,
"timeout": 300,
}),
notes: vec![],
},
Transport::Stdio { db_path } => CompiledEntry {
name: String::new(),
top_key: "extensions".into(),
value: serde_json::json!({
"name": "lific",
"type": "stdio",
"cmd": "lific",
"args": stdio_args(db_path),
"enabled": true,
"timeout": 300,
}),
notes: vec![
"Goose config is YAML; existing comments in config.yaml are not preserved \
across this edit."
.into(),
],
},
},
},
ClientSpec {
id: "crush",
display: "Crush",
oauth: OauthSupport::Unsupported {
reason: "Crush's MCP OAuth support is unconfirmed — use the default key mode \
(drop --oauth) to connect it.",
},
format: Format::Json,
global_path: |b| Some(config_dir(b, &["crush", "crush.json"])),
project_path: |b| Some(project_rel(b, &["crush.json"])),
detect_extra: |b, scope| match scope {
Scope::Global => vec![config_dir(b, &["crush"])],
Scope::Project => vec![],
},
compile: |c| match &c.transport {
Transport::Remote { url, key } => CompiledEntry {
name: String::new(),
top_key: "mcp".into(),
value: serde_json::json!({
"type": "http",
"url": url,
"headers": headers_obj(key),
}),
notes: vec![],
},
Transport::OauthRemote { url } => CompiledEntry {
name: String::new(),
top_key: "mcp".into(),
value: serde_json::json!({
"type": "http",
"url": url,
}),
notes: vec![],
},
Transport::Stdio { db_path } => CompiledEntry {
name: String::new(),
top_key: "mcp".into(),
value: serde_json::json!({
"type": "stdio",
"command": "lific",
"args": stdio_args(db_path),
}),
notes: vec![],
},
},
},
]
}
pub fn find_client(id: &str) -> Option<ClientSpec> {
all_clients().into_iter().find(|c| c.id == id)
}
pub fn all_client_ids() -> Vec<&'static str> {
all_clients().iter().map(|c| c.id).collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn linux_base() -> PathBase {
PathBase {
home: PathBuf::from("/home/tester"),
project: PathBuf::from("/proj"),
os: Os::Linux,
appdata: None,
}
}
fn remote_cfg() -> ServerConfig {
ServerConfig::remote("http://127.0.0.1:3456/mcp", "lific_sk-live-KEY")
}
fn stdio_cfg() -> ServerConfig {
ServerConfig::stdio("/abs/lific.db")
}
fn oauth_cfg() -> ServerConfig {
ServerConfig::oauth_remote("http://127.0.0.1:3456/mcp")
}
#[test]
fn opencode_oauth_has_url_but_no_headers() {
let e = find_client("opencode").unwrap().compile(&oauth_cfg());
assert_eq!(e.value["url"], "http://127.0.0.1:3456/mcp");
assert!(
e.value.get("headers").is_none(),
"oauth opencode entry must have no headers"
);
assert_eq!(e.value["type"], "remote");
}
#[test]
fn codex_oauth_has_url_and_no_bearer_env_var() {
let e = find_client("codex").unwrap().compile(&oauth_cfg());
assert_eq!(e.value["url"], "http://127.0.0.1:3456/mcp");
assert!(
e.value.get("bearer_token_env_var").is_none(),
"oauth codex entry must not set bearer_token_env_var"
);
}
#[test]
fn oauth_capable_clients_have_hints_incapable_have_reasons() {
let capable = [
"opencode",
"claude-code",
"cursor",
"vscode",
"codex",
"zed",
"gemini",
"windsurf",
];
for id in capable {
match find_client(id).unwrap().oauth {
OauthSupport::Capable { hint } => assert!(!hint.is_empty(), "{id} needs a hint"),
OauthSupport::Unsupported { .. } => panic!("{id} should be OAuth-capable"),
}
}
for id in ["claude-desktop", "goose", "crush"] {
match find_client(id).unwrap().oauth {
OauthSupport::Unsupported { reason } => {
assert!(!reason.is_empty(), "{id} needs a reason")
}
OauthSupport::Capable { .. } => panic!("{id} should not be OAuth-capable"),
}
}
}
#[test]
fn every_client_has_a_global_path_or_documents_why_not() {
let base = linux_base();
for c in all_clients() {
assert!(
c.path_for(&base, Scope::Global).is_some(),
"{} must have a global path",
c.id
);
}
}
#[test]
fn opencode_project_path_is_bare_opencode_json() {
let base = linux_base();
let oc = find_client("opencode").unwrap();
assert_eq!(
oc.path_for(&base, Scope::Project).unwrap(),
PathBuf::from("/proj/opencode.json")
);
assert_eq!(
oc.path_for(&base, Scope::Global).unwrap(),
PathBuf::from("/home/tester/.config/opencode/opencode.json")
);
}
#[test]
fn opencode_remote_uses_type_remote_and_headers() {
let e = find_client("opencode").unwrap().compile(&remote_cfg());
assert_eq!(e.top_key, "mcp");
assert_eq!(e.value["type"], "remote");
assert_eq!(e.value["url"], "http://127.0.0.1:3456/mcp");
assert_eq!(e.value["headers"]["Authorization"], "Bearer lific_sk-live-KEY");
assert_eq!(e.value["enabled"], true);
}
#[test]
fn opencode_stdio_uses_type_local_and_command_array() {
let e = find_client("opencode").unwrap().compile(&stdio_cfg());
assert_eq!(e.value["type"], "local");
assert_eq!(
e.value["command"],
serde_json::json!(["lific", "--db", "/abs/lific.db", "mcp"])
);
}
#[test]
fn claude_code_paths_and_http_type() {
let base = linux_base();
let c = find_client("claude-code").unwrap();
assert_eq!(
c.path_for(&base, Scope::Global).unwrap(),
PathBuf::from("/home/tester/.claude.json")
);
assert_eq!(
c.path_for(&base, Scope::Project).unwrap(),
PathBuf::from("/proj/.mcp.json")
);
let e = c.compile(&remote_cfg());
assert_eq!(e.top_key, "mcpServers");
assert_eq!(e.value["type"], "http");
}
#[test]
fn claude_desktop_remote_uses_mcp_remote_shim() {
let e = find_client("claude-desktop").unwrap().compile(&remote_cfg());
assert_eq!(e.value["command"], "npx");
let args = e.value["args"].as_array().unwrap();
assert!(args.iter().any(|a| a == "mcp-remote"));
assert!(
args.iter()
.any(|a| a.as_str() == Some("Authorization: Bearer lific_sk-live-KEY"))
);
assert!(!e.notes.is_empty(), "shim note should be present");
}
#[test]
fn claude_desktop_has_no_project_path() {
let base = linux_base();
assert!(
find_client("claude-desktop")
.unwrap()
.path_for(&base, Scope::Project)
.is_none()
);
}
#[test]
fn claude_desktop_mac_path_is_application_support() {
let base = PathBase {
home: PathBuf::from("/Users/tester"),
project: PathBuf::from("/proj"),
os: Os::Mac,
appdata: None,
};
assert_eq!(
find_client("claude-desktop")
.unwrap()
.path_for(&base, Scope::Global)
.unwrap(),
PathBuf::from(
"/Users/tester/Library/Application Support/Claude/claude_desktop_config.json"
)
);
}
#[test]
fn claude_desktop_windows_path_uses_appdata() {
let base = PathBase {
home: PathBuf::from("C:\\Users\\tester"),
project: PathBuf::from("C:\\proj"),
os: Os::Windows,
appdata: Some(PathBuf::from("C:\\Users\\tester\\AppData\\Roaming")),
};
assert_eq!(
find_client("claude-desktop")
.unwrap()
.path_for(&base, Scope::Global)
.unwrap(),
PathBuf::from(
"C:\\Users\\tester\\AppData\\Roaming/Claude/claude_desktop_config.json"
)
);
}
#[test]
fn vscode_uses_servers_key_not_mcpservers() {
let e = find_client("vscode").unwrap().compile(&remote_cfg());
assert_eq!(e.top_key, "servers");
assert_eq!(e.value["type"], "http");
}
#[test]
fn vscode_mac_global_path_is_application_support() {
let base = PathBase {
home: PathBuf::from("/Users/tester"),
project: PathBuf::from("/proj"),
os: Os::Mac,
appdata: None,
};
assert_eq!(
find_client("vscode")
.unwrap()
.path_for(&base, Scope::Global)
.unwrap(),
PathBuf::from("/Users/tester/Library/Application Support/Code/User/mcp.json")
);
}
#[test]
fn cursor_remote_uses_bare_url_and_headers() {
let e = find_client("cursor").unwrap().compile(&remote_cfg());
assert_eq!(e.top_key, "mcpServers");
assert_eq!(e.value["url"], "http://127.0.0.1:3456/mcp");
assert!(e.value.get("type").is_none());
}
#[test]
fn gemini_remote_uses_httpurl_not_url() {
let e = find_client("gemini").unwrap().compile(&remote_cfg());
assert_eq!(e.value["httpUrl"], "http://127.0.0.1:3456/mcp");
assert!(
e.value.get("url").is_none(),
"gemini must not use the `url` key"
);
}
#[test]
fn windsurf_remote_uses_serverurl_not_url() {
let e = find_client("windsurf").unwrap().compile(&remote_cfg());
assert_eq!(e.value["serverUrl"], "http://127.0.0.1:3456/mcp");
assert!(
e.value.get("url").is_none(),
"windsurf must not use the `url` key"
);
}
#[test]
fn windsurf_has_no_project_path() {
let base = linux_base();
assert!(
find_client("windsurf")
.unwrap()
.path_for(&base, Scope::Project)
.is_none()
);
}
#[test]
fn codex_remote_uses_env_var_and_dotted_key() {
let e = find_client("codex").unwrap().compile(&remote_cfg());
assert_eq!(e.top_key, "mcp_servers.lific");
assert_eq!(e.value["url"], "http://127.0.0.1:3456/mcp");
assert_eq!(e.value["bearer_token_env_var"], "LIFIC_API_KEY");
assert!(
!e.value.to_string().contains("lific_sk-live-KEY"),
"codex must not inline the bearer key"
);
assert!(!e.notes.is_empty());
}
#[test]
fn zed_uses_context_servers_key() {
let e = find_client("zed").unwrap().compile(&remote_cfg());
assert_eq!(e.top_key, "context_servers");
}
#[test]
fn goose_remote_uses_streamable_http_and_uri() {
let e = find_client("goose").unwrap().compile(&remote_cfg());
assert_eq!(e.top_key, "extensions");
assert_eq!(e.value["type"], "streamable_http");
assert_eq!(e.value["uri"], "http://127.0.0.1:3456/mcp");
assert_eq!(e.value["enabled"], true);
assert_eq!(e.value["timeout"], 300);
}
#[test]
fn crush_remote_uses_mcp_key_and_http_type() {
let base = linux_base();
let c = find_client("crush").unwrap();
assert_eq!(
c.path_for(&base, Scope::Project).unwrap(),
PathBuf::from("/proj/crush.json")
);
let e = c.compile(&remote_cfg());
assert_eq!(e.top_key, "mcp");
assert_eq!(e.value["type"], "http");
}
#[test]
fn find_client_unknown_is_none() {
assert!(find_client("notaclient").is_none());
}
#[test]
fn all_client_ids_are_unique() {
let ids = all_client_ids();
let mut sorted = ids.clone();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(ids.len(), sorted.len(), "client ids must be unique");
}
}