use crate::error::{Error, Result};
use crate::mcp::McpClientConfig;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use tracing::{debug, info, warn};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConfigEntry {
pub name: String,
pub command: String,
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub env: HashMap<String, String>,
#[serde(default = "default_timeout")]
pub timeout_secs: u64,
#[serde(default)]
pub auto_reconnect: bool,
#[serde(default = "default_retries")]
pub max_retries: u32,
#[serde(default)]
pub tools: Vec<String>,
}
fn default_timeout() -> u64 {
30
}
fn default_retries() -> u32 {
3
}
impl From<ServerConfigEntry> for McpClientConfig {
fn from(entry: ServerConfigEntry) -> Self {
McpClientConfig {
name: entry.name,
command: entry.command,
args: entry.args,
env: entry.env,
timeout_secs: entry.timeout_secs,
auto_reconnect: entry.auto_reconnect,
max_retries: entry.max_retries,
}
}
}
#[derive(Debug, Clone)]
pub struct ServerRegistry {
tool_servers: HashMap<String, ServerConfigEntry>,
servers: HashMap<String, ServerConfigEntry>,
}
impl ServerRegistry {
pub fn new() -> Self {
let mut registry = Self {
tool_servers: HashMap::new(),
servers: HashMap::new(),
};
registry.register_builtin_servers();
registry
}
pub fn load() -> Result<Self> {
let mut registry = Self::new();
if let Some(config_path) = Self::get_config_path() {
if config_path.exists() {
match registry.load_from_file(&config_path) {
Ok(count) => {
info!(path = %config_path.display(), servers = count, "Loaded server config");
}
Err(e) => {
warn!(error = %e, "Failed to load server config, using built-ins only");
}
}
} else {
debug!(
"No user config found at {}, using built-ins",
config_path.display()
);
}
}
Ok(registry)
}
fn get_config_path() -> Option<PathBuf> {
dirs::config_dir().map(|d| d.join("reasonkit").join("mcp_servers.json"))
}
fn load_from_file(&mut self, path: &PathBuf) -> Result<usize> {
let content = std::fs::read_to_string(path)?;
let entries: Vec<ServerConfigEntry> = serde_json::from_str(&content)
.map_err(|e| Error::config(format!("Invalid server config: {}", e)))?;
let count = entries.len();
for entry in entries {
self.register_server(entry);
}
Ok(count)
}
fn register_builtin_servers(&mut self) {
let thinktools = ServerConfigEntry {
name: "reasonkit-thinktools".to_string(),
command: "rk".to_string(),
args: vec!["serve-mcp".to_string()],
env: HashMap::new(),
timeout_secs: 30,
auto_reconnect: false,
max_retries: 1,
tools: vec![
"gigathink".to_string(),
"laserlogic".to_string(),
"bedrock".to_string(),
"proofguard".to_string(),
"brutalhonesty".to_string(),
],
};
self.register_server(thinktools);
let sequential = ServerConfigEntry {
name: "reasonkit-sequential".to_string(),
command: "rk-core".to_string(),
args: vec!["serve-mcp".to_string()],
env: HashMap::new(),
timeout_secs: 30,
auto_reconnect: false,
max_retries: 1,
tools: vec![
"think".to_string(),
"analyze".to_string(),
"reason".to_string(),
"reflect".to_string(),
],
};
self.register_server(sequential);
let memory = ServerConfigEntry {
name: "reasonkit-memory".to_string(),
command: "rk-mem".to_string(),
args: vec!["serve".to_string()],
env: HashMap::new(),
timeout_secs: 30,
auto_reconnect: true,
max_retries: 3,
tools: vec![
"store".to_string(),
"search".to_string(),
"retrieve".to_string(),
"forget".to_string(),
],
};
self.register_server(memory);
let web = ServerConfigEntry {
name: "reasonkit-web".to_string(),
command: "rk-web".to_string(),
args: vec!["serve-mcp".to_string()],
env: HashMap::new(),
timeout_secs: 60,
auto_reconnect: true,
max_retries: 2,
tools: vec![
"browse".to_string(),
"screenshot".to_string(),
"extract".to_string(),
"fetch".to_string(),
],
};
self.register_server(web);
}
fn register_server(&mut self, entry: ServerConfigEntry) {
for tool in &entry.tools {
self.tool_servers.insert(tool.clone(), entry.clone());
}
self.servers.insert(entry.name.clone(), entry);
}
pub fn get_server_for_tool(&self, tool_name: &str) -> Option<&ServerConfigEntry> {
self.tool_servers.get(tool_name)
}
pub fn get_server(&self, server_name: &str) -> Option<&ServerConfigEntry> {
self.servers.get(server_name)
}
pub fn get_client_config(&self, tool_name: &str) -> Option<McpClientConfig> {
self.get_server_for_tool(tool_name)
.map(|entry| entry.clone().into())
}
pub fn list_tools(&self) -> Vec<&str> {
self.tool_servers.keys().map(|s| s.as_str()).collect()
}
pub fn list_servers(&self) -> Vec<&str> {
self.servers.keys().map(|s| s.as_str()).collect()
}
}
impl Default for ServerRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_builtin_thinktools() {
let registry = ServerRegistry::new();
assert!(registry.get_server_for_tool("gigathink").is_some());
assert!(registry.get_server_for_tool("laserlogic").is_some());
assert!(registry.get_server_for_tool("bedrock").is_some());
assert!(registry.get_server_for_tool("proofguard").is_some());
assert!(registry.get_server_for_tool("brutalhonesty").is_some());
}
#[test]
fn test_builtin_sequential() {
let registry = ServerRegistry::new();
assert!(registry.get_server_for_tool("think").is_some());
assert!(registry.get_server_for_tool("analyze").is_some());
assert!(registry.get_server_for_tool("reason").is_some());
}
#[test]
fn test_unknown_tool() {
let registry = ServerRegistry::new();
assert!(registry.get_server_for_tool("unknown_tool").is_none());
}
#[test]
fn test_client_config_conversion() {
let registry = ServerRegistry::new();
let config = registry.get_client_config("gigathink").unwrap();
assert_eq!(config.name, "reasonkit-thinktools");
assert_eq!(config.command, "rk");
assert_eq!(config.args, vec!["serve-mcp"]);
}
#[test]
fn test_list_tools() {
let registry = ServerRegistry::new();
let tools = registry.list_tools();
assert!(tools.contains(&"gigathink"));
assert!(tools.contains(&"think"));
assert!(tools.contains(&"store"));
assert!(tools.contains(&"browse"));
}
#[test]
fn test_config_entry_serialization() {
let entry = ServerConfigEntry {
name: "test".to_string(),
command: "test-cmd".to_string(),
args: vec!["--arg".to_string()],
env: HashMap::new(),
timeout_secs: 30,
auto_reconnect: true,
max_retries: 3,
tools: vec!["tool1".to_string()],
};
let json = serde_json::to_string(&entry).unwrap();
let parsed: ServerConfigEntry = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.name, "test");
assert_eq!(parsed.timeout_secs, 30);
}
#[test]
fn test_config_entry_defaults() {
let json = r#"{"name": "test", "command": "cmd"}"#;
let entry: ServerConfigEntry = serde_json::from_str(json).unwrap();
assert_eq!(entry.timeout_secs, 30); assert_eq!(entry.max_retries, 3); assert!(!entry.auto_reconnect); assert!(entry.tools.is_empty()); }
}