Files
secrets/crates/client-integrations/src/lib.rs
agent 0374899dab
Some checks failed
Secrets v3 CI / 检查 (push) Has been cancelled
feat(v3): migrate workspace to API, Tauri desktop, and v3 crates; remove legacy MCP stack
- Add apps/api, desktop Tauri shell, domain/application/crypto/device-auth/infrastructure-db
- Replace desktop-daemon vault integration; drop secrets-core and secrets-mcp*
- Ignore apps/desktop/dist and generated Tauri icons; document icon/dist steps in AGENTS.md
- Apply rustfmt; fix clippy (collapsible_if, HTTP method as str)
2026-04-14 17:37:12 +08:00

163 lines
4.7 KiB
Rust

use anyhow::{Context, Result};
use serde_json::{Map, Value};
use std::{
fs,
path::{Path, PathBuf},
};
pub trait ClientAdapter {
fn client_name(&self) -> &'static str;
fn config_path(&self) -> PathBuf;
}
pub struct CursorAdapter;
impl ClientAdapter for CursorAdapter {
fn client_name(&self) -> &'static str {
"cursor"
}
fn config_path(&self) -> PathBuf {
default_home().join(".cursor").join("mcp.json")
}
}
pub struct ClaudeCodeAdapter;
impl ClientAdapter for ClaudeCodeAdapter {
fn client_name(&self) -> &'static str {
"claude-code"
}
fn config_path(&self) -> PathBuf {
default_home().join(".claude").join("mcp.json")
}
}
fn default_home() -> PathBuf {
std::env::var_os("HOME")
.or_else(|| std::env::var_os("USERPROFILE"))
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("."))
}
pub fn has_managed_server(adapter: &dyn ClientAdapter, server_name: &str) -> Result<bool> {
let path = adapter.config_path();
let root = read_config_or_default(&path)?;
Ok(root
.get("mcpServers")
.and_then(Value::as_object)
.is_some_and(|servers| servers.contains_key(server_name)))
}
pub fn upsert_managed_server(
adapter: &dyn ClientAdapter,
server_name: &str,
server_config: Value,
) -> Result<()> {
let path = adapter.config_path();
let mut root = read_config_or_default(&path)?;
let root_object = ensure_object(&mut root);
let mcp_servers = root_object
.entry("mcpServers".to_string())
.or_insert_with(|| Value::Object(Map::new()));
let servers_object = ensure_object(mcp_servers);
servers_object.insert(server_name.to_string(), server_config);
write_config_atomically(&path, &root)
}
fn read_config_or_default(path: &Path) -> Result<Value> {
if !path.exists() {
return Ok(Value::Object(Map::new()));
}
let raw =
fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?;
serde_json::from_str(&raw).with_context(|| format!("failed to parse {}", path.display()))
}
fn write_config_atomically(path: &Path, value: &Value) -> Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("failed to create {}", parent.display()))?;
}
let tmp_path = path.with_extension("json.tmp");
let body = serde_json::to_string_pretty(value).context("failed to serialize mcp config")?;
fs::write(&tmp_path, body)
.with_context(|| format!("failed to write {}", tmp_path.display()))?;
fs::rename(&tmp_path, path).with_context(|| format!("failed to replace {}", path.display()))?;
Ok(())
}
fn ensure_object(value: &mut Value) -> &mut Map<String, Value> {
if !value.is_object() {
*value = Value::Object(Map::new());
}
value.as_object_mut().expect("object just ensured")
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::{SystemTime, UNIX_EPOCH};
struct TestAdapter {
path: PathBuf,
}
impl ClientAdapter for TestAdapter {
fn client_name(&self) -> &'static str {
"test"
}
fn config_path(&self) -> PathBuf {
self.path.clone()
}
}
#[test]
fn upsert_preserves_other_servers() {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock")
.as_nanos();
let base = std::env::temp_dir().join(format!("secrets-client-integrations-{unique}"));
let adapter = TestAdapter {
path: base.join("mcp.json"),
};
fs::create_dir_all(adapter.path.parent().expect("parent")).expect("mkdir");
fs::write(
&adapter.path,
r#"{"mcpServers":{"postgres":{"command":"npx"},"secrets":{"url":"http://old"}}}"#,
)
.expect("seed config");
upsert_managed_server(
&adapter,
"secrets",
serde_json::json!({
"url": "http://127.0.0.1:9515/mcp"
}),
)
.expect("upsert config");
let root: Value =
serde_json::from_str(&fs::read_to_string(&adapter.path).expect("read back"))
.expect("parse back");
let servers = root
.get("mcpServers")
.and_then(Value::as_object)
.expect("mcpServers object");
assert!(servers.contains_key("postgres"));
assert_eq!(
servers
.get("secrets")
.and_then(Value::as_object)
.and_then(|value| value.get("url"))
.and_then(Value::as_str),
Some("http://127.0.0.1:9515/mcp")
);
let _ = fs::remove_dir_all(base);
}
}