#[cfg(not(unix))]
compile_error!("mcp-valve requires a Unix platform (Linux, macOS, BSD)");
use anyhow::{anyhow, Context, Result};
use clap::{Parser, Subcommand};
use nix::sys::signal::{kill, Signal};
use nix::sys::stat::{umask, Mode};
use nix::unistd::{setsid, Pid};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::collections::HashMap;
use std::fs;
use std::io::{BufRead, BufReader, Read, Write};
use std::os::unix::fs::PermissionsExt;
use std::os::unix::net::{UnixListener, UnixStream};
use std::os::unix::process::CommandExt;
use std::path::PathBuf;
use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
use std::time::Duration;
#[derive(Debug, Deserialize, Serialize, Clone)]
struct ServerProfile {
command: Vec<String>,
#[serde(default)]
default_args: Vec<String>,
#[serde(default)]
supports_daemon: bool,
#[serde(default)]
description: String,
#[serde(default)]
env: HashMap<String, String>,
}
#[derive(Debug, Deserialize)]
struct ServerConfig {
#[serde(flatten)]
servers: HashMap<String, ServerProfile>,
}
fn get_config_path(cli_config: Option<PathBuf>) -> Result<PathBuf> {
if let Some(path) = cli_config {
return Ok(path);
}
if let Ok(path) = std::env::var("MCP_VALVE_CONFIG") {
return Ok(PathBuf::from(path));
}
let home = std::env::var("HOME").context("HOME environment variable not set")?;
if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
let path = PathBuf::from(xdg).join("mcp-valve/servers.json");
if path.exists() {
return Ok(path);
}
}
let xdg_default = PathBuf::from(&home).join(".config/mcp-valve/servers.json");
if xdg_default.exists() {
return Ok(xdg_default);
}
Ok(PathBuf::from(&home).join(".claude/scripts/mcp-servers.json"))
}
fn load_server_config(cli_config: Option<PathBuf>) -> Result<ServerConfig> {
let config_path = get_config_path(cli_config)?;
if !config_path.exists() {
let home = std::env::var("HOME").unwrap_or_default();
return Err(anyhow!(
"Configuration file not found.\n\n\
Searched locations (in order):\n \
1. --config flag or MCP_VALVE_CONFIG env var\n \
2. $XDG_CONFIG_HOME/mcp-valve/servers.json\n \
3. ~/.config/mcp-valve/servers.json\n \
4. ~/.claude/scripts/mcp-servers.json\n\n\
Create a config file at: {}\n\n\
Example:\n\
{{\n \
\"server-name\": {{\n \
\"command\": [\"npx\", \"@example/mcp-server\"],\n \
\"default_args\": [],\n \
\"supports_daemon\": true,\n \
\"description\": \"Example MCP server\",\n \
\"env\": {{}}\n \
}}\n\
}}",
PathBuf::from(&home).join(".config/mcp-valve/servers.json").display()
));
}
let config_content = fs::read_to_string(&config_path)
.with_context(|| format!("Failed to read config: {}", config_path.display()))?;
let config: ServerConfig = serde_json::from_str(&config_content)
.with_context(|| format!("Invalid JSON in config: {}", config_path.display()))?;
Ok(config)
}
#[derive(Parser)]
#[command(name = "mcp-valve")]
#[command(about = "Unified MCP CLI - Generic MCP Protocol Client")]
#[command(version)]
struct Cli {
#[arg(short, long)]
server: Option<String>,
#[arg(long)]
server_args: Option<String>,
#[arg(short, long, global = true)]
config: Option<PathBuf>,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
ListServers,
Call {
tool: String,
#[arg(short, long, default_value = "{}")]
args: String,
},
ListTools,
Shell,
StartDaemon,
StopDaemon,
DaemonStatus,
}
fn sanitize_server_name(name: &str) -> String {
name.chars()
.filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_')
.collect()
}
fn expand_template_vars(arg: &str, server_name: &str) -> String {
let safe_server_name = sanitize_server_name(server_name);
let profile_dir = PathBuf::from(".mcp-profile").join(&safe_server_name);
let profile_dir_str = profile_dir.to_str().unwrap_or("");
let pid = std::process::id().to_string();
let cwd = std::env::current_dir()
.ok()
.and_then(|p| p.to_str().map(|s| s.to_string()))
.unwrap_or_else(|| ".".to_string());
arg.replace("{profile_dir}", profile_dir_str)
.replace("{pid}", &pid)
.replace("{cwd}", &cwd)
}
struct McpClient {
child: Child,
stdin: ChildStdin,
stdout: BufReader<ChildStdout>,
request_id: u64,
}
impl McpClient {
fn start(profile: &ServerProfile, extra_args: Option<Vec<String>>, server_name: &str) -> Result<Self> {
eprintln!("🚀 Starting MCP server...");
if profile.command.is_empty() {
return Err(anyhow!("Server profile has empty command"));
}
let mut cmd = Command::new(&profile.command[0]);
if profile.command.len() > 1 {
cmd.args(&profile.command[1..]);
}
let args_to_use = match extra_args {
Some(args) => args.iter().map(|arg| expand_template_vars(arg, server_name)).collect(),
None => profile.default_args.iter().map(|arg| expand_template_vars(arg, server_name)).collect::<Vec<String>>(),
};
cmd.args(&args_to_use);
for (key, value) in &profile.env {
cmd.env(key, value);
}
let mut child = cmd
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.with_context(|| format!("Failed to spawn MCP server: {:?}", profile.command))?;
let stdin = child.stdin.take().unwrap();
let stdout = BufReader::new(child.stdout.take().unwrap());
let mut mcp = Self {
child,
stdin,
stdout,
request_id: 0,
};
mcp.initialize()?;
eprintln!("✅ MCP server ready");
Ok(mcp)
}
fn initialize(&mut self) -> Result<()> {
let init_request = json!({
"jsonrpc": "2.0",
"id": self.next_id(),
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": {
"name": "mcp-valve",
"version": env!("CARGO_PKG_VERSION")
}
}
});
self.send_request(&init_request)?;
let notification = json!({
"jsonrpc": "2.0",
"method": "notifications/initialized",
"params": {}
});
self.send_notification(¬ification)?;
Ok(())
}
fn send_request(&mut self, request: &Value) -> Result<Value> {
let request_str = serde_json::to_string(request)?;
writeln!(self.stdin, "{}", request_str)?;
self.stdin.flush()?;
let mut line = String::new();
self.stdout.read_line(&mut line)?;
let response: Value = serde_json::from_str(line.trim())
.context("Failed to parse JSON-RPC response")?;
if let Some(error) = response.get("error") {
return Err(anyhow!("MCP Error: {}", error));
}
Ok(response)
}
fn send_notification(&mut self, notification: &Value) -> Result<()> {
let notif_str = serde_json::to_string(notification)?;
writeln!(self.stdin, "{}", notif_str)?;
self.stdin.flush()?;
Ok(())
}
fn next_id(&mut self) -> u64 {
self.request_id += 1;
self.request_id
}
fn call_tool(&mut self, name: &str, args: Value) -> Result<Value> {
let request = json!({
"jsonrpc": "2.0",
"id": self.next_id(),
"method": "tools/call",
"params": {
"name": name,
"arguments": args
}
});
let response = match self.send_request(&request) {
Ok(resp) => resp,
Err(e) => {
let error_with_schema = self.format_error_with_schema(name, &e.to_string());
return Err(anyhow!("{}", error_with_schema));
}
};
let result = response["result"].clone();
if let Some(is_error) = result.get("isError").and_then(|v| v.as_bool()) {
if is_error {
let error_msg = result
.get("content")
.and_then(|c| c.as_array())
.and_then(|arr| arr.first())
.and_then(|item| item.get("text"))
.and_then(|t| t.as_str())
.unwrap_or("Tool execution failed");
let error_with_schema =
self.format_error_with_schema(name, &format!("Tool Error: {}", error_msg));
return Err(anyhow!("{}", error_with_schema));
}
}
Ok(result)
}
fn list_tools(&mut self) -> Result<Value> {
let request = json!({
"jsonrpc": "2.0",
"id": self.next_id(),
"method": "tools/list",
"params": {}
});
let response = self.send_request(&request)?;
Ok(response["result"].clone())
}
fn get_tool_schema(&mut self, tool_name: &str) -> Option<Value> {
self.list_tools()
.ok()
.and_then(|result| result.get("tools").cloned())
.and_then(|tools| tools.as_array().cloned())
.and_then(|tools| {
tools
.into_iter()
.find(|t| t.get("name").and_then(|n| n.as_str()) == Some(tool_name))
})
.and_then(|tool| tool.get("inputSchema").cloned())
}
fn format_error_with_schema(&mut self, tool_name: &str, error_msg: &str) -> String {
match self.get_tool_schema(tool_name) {
Some(schema) => {
let schema_str = serde_json::to_string_pretty(&schema)
.unwrap_or_else(|_| schema.to_string());
format!(
"{}\n\nSchema for tool '{}':\n{}",
error_msg, tool_name, schema_str
)
}
None => error_msg.to_string(),
}
}
}
impl Drop for McpClient {
fn drop(&mut self) {
let _ = self.child.kill();
}
}
fn get_project_path() -> String {
std::env::current_dir()
.ok()
.and_then(|p| p.to_str().map(|s| s.to_string()))
.unwrap_or_else(|| ".".to_string())
}
fn daemon_not_running_error(server_name: &str) -> anyhow::Error {
let project = get_project_path();
anyhow!(
"Daemon is not running for project '{}'\n\n\
Start daemon with:\n \
cd {}\n \
mcp-valve --server {} start-daemon",
project, project, server_name
)
}
struct DaemonManager {
server_name: String,
pid_file: PathBuf,
}
impl DaemonManager {
fn new(server_name: &str) -> Self {
let safe_server_name = sanitize_server_name(server_name);
let profile_dir = PathBuf::from(".mcp-profile")
.join(&safe_server_name);
if !profile_dir.exists() {
let old_umask = umask(Mode::from_bits_truncate(0o077));
fs::create_dir_all(&profile_dir)
.expect("Failed to create daemon profile directory");
umask(old_umask);
}
Self {
server_name: server_name.to_string(),
pid_file: profile_dir.join("daemon.pid"),
}
}
fn get_socket_path(&self) -> Result<PathBuf> {
let pid_str = fs::read_to_string(&self.pid_file)
.context("Failed to read PID file")?;
let pid = pid_str.trim();
Ok(PathBuf::from("/tmp/.mcp").join(format!("{}-{}.sock", self.server_name, pid)))
}
fn is_running(&self) -> Result<bool> {
if !self.pid_file.exists() {
return Ok(false);
}
let pid_str = fs::read_to_string(&self.pid_file)
.context("Failed to read PID file")?;
let pid = pid_str.trim().parse::<i32>()
.with_context(|| format!("Invalid PID in file: '{}'", pid_str.trim()))?;
match kill(Pid::from_raw(pid), None) {
Ok(_) => Ok(true), Err(nix::errno::Errno::ESRCH) => Ok(false), Err(nix::errno::Errno::EPERM) => Ok(true), Err(_) => Ok(false), }
}
fn start(
&self,
profile: &ServerProfile,
extra_args: Option<Vec<String>>,
) -> Result<()> {
if !profile.supports_daemon {
return Err(anyhow!(
"Server '{}' does not support daemon mode (supports_daemon: false)",
self.server_name
));
}
if self.is_running()? {
return Err(anyhow!("Daemon already running for '{}'", self.server_name));
}
let project = get_project_path();
eprintln!("Project: {}", project);
eprintln!("Profile: {}", self.pid_file.parent().unwrap().display());
eprintln!("Starting MCP daemon for '{}'...", self.server_name);
let mut cmd = Command::new(std::env::current_exe()?);
cmd.arg("__internal_daemon");
cmd.arg("--server");
cmd.arg(&self.server_name);
if let Some(ref args) = extra_args {
cmd.arg("--server-args");
cmd.arg(serde_json::to_string(args)?);
}
let profile_dir = self.pid_file.parent().unwrap();
let log_file = std::fs::File::create(profile_dir.join("daemon.log"))
.context("Failed to create daemon log file")?;
let child = unsafe {
cmd.pre_exec(|| {
setsid().map_err(|e| std::io::Error::from_raw_os_error(e as i32))?;
Ok(())
})
}
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::from(log_file))
.spawn()
.context("Failed to spawn daemon process")?;
let child_pid = child.id();
fs::write(&self.pid_file, child_pid.to_string())
.context("Failed to write PID file")?;
let expected_socket = PathBuf::from("/tmp/.mcp")
.join(format!("{}-{}.sock", self.server_name, child_pid));
for i in 0..50 {
if expected_socket.exists() {
eprintln!("Daemon started (PID: {})", child_pid);
eprintln!("Socket: {}", expected_socket.display());
return Ok(());
}
std::thread::sleep(Duration::from_millis(100));
if i == 20 {
if kill(Pid::from_raw(child_pid as i32), None).is_err() {
fs::remove_file(&self.pid_file).ok();
return Err(anyhow!(
"Daemon process exited unexpectedly. Check {}/daemon.log",
profile_dir.display()
));
}
}
}
fs::remove_file(&self.pid_file).ok();
Err(anyhow!(
"Daemon failed to start - socket file not created within 5 seconds. Check {}/daemon.log",
profile_dir.display()
))
}
fn stop(&self) -> Result<()> {
if !self.is_running()? {
return Err(daemon_not_running_error(&self.server_name));
}
let project = get_project_path();
let pid_str = fs::read_to_string(&self.pid_file)?;
let pid: i32 = pid_str.trim().parse()
.context("Invalid PID in file")?;
let socket_path = self.get_socket_path().ok();
eprintln!("Project: {}", project);
eprintln!("Stopping daemon (PID: {})...", pid);
kill(Pid::from_raw(pid), Signal::SIGTERM)
.context("Failed to send SIGTERM")?;
for _ in 0..10 {
if !self.is_running()? {
fs::remove_file(&self.pid_file).ok();
if let Some(ref sp) = socket_path {
if sp.exists() {
fs::remove_file(sp).ok();
}
}
eprintln!("Daemon stopped");
return Ok(());
}
std::thread::sleep(Duration::from_millis(500));
}
kill(Pid::from_raw(pid), Signal::SIGKILL)
.context("Failed to send SIGKILL")?;
fs::remove_file(&self.pid_file).ok();
if let Some(ref sp) = socket_path {
if sp.exists() {
fs::remove_file(sp).ok();
}
}
eprintln!("Daemon stopped (forced)");
Ok(())
}
fn status(&self) -> Result<()> {
let project = get_project_path();
let profile_dir = self.pid_file.parent().unwrap();
println!("Project: {}", project);
println!("Server: {}", self.server_name);
println!("Profile: {}", profile_dir.display());
if self.is_running()? {
let pid_str = fs::read_to_string(&self.pid_file)?;
let socket_path = self.get_socket_path()?;
println!("Daemon is running");
println!(" PID: {}", pid_str.trim());
println!(" Socket: {}", socket_path.display());
} else {
println!("Daemon is not running");
if self.pid_file.exists() {
eprintln!("Warning: Stale PID file found, cleaning up...");
let socket_path = self.get_socket_path().ok();
fs::remove_file(&self.pid_file).ok();
if let Some(sp) = socket_path {
if sp.exists() {
fs::remove_file(&sp).ok();
}
}
}
}
Ok(())
}
}
fn run_daemon(server_name: &str, profile: &ServerProfile, extra_args: Option<Vec<String>>) -> Result<()> {
let socket_dir = PathBuf::from("/tmp/.mcp");
if !socket_dir.exists() {
let old_umask = umask(Mode::from_bits_truncate(0o077));
fs::create_dir_all(&socket_dir)
.context("Failed to create socket directory")?;
umask(old_umask);
}
let socket_path = socket_dir.join(format!("{}-{}.sock", server_name, std::process::id()));
if socket_path.exists() {
fs::remove_file(&socket_path)?;
}
let listener = UnixListener::bind(&socket_path)
.context("Failed to bind Unix socket")?;
fs::set_permissions(&socket_path, fs::Permissions::from_mode(0o600))
.context("Failed to set socket permissions")?;
eprintln!("Daemon listening on {:?}", socket_path);
let mut mcp = McpClient::start(profile, extra_args, server_name)?;
for stream in listener.incoming() {
match stream {
Ok(stream) => {
if let Err(e) = handle_client(&mut mcp, stream) {
eprintln!("Client error: {}", e);
}
}
Err(e) => {
eprintln!("Connection error: {}", e);
}
}
}
Ok(())
}
fn handle_client(mcp: &mut McpClient, mut stream: UnixStream) -> Result<()> {
const MAX_REQUEST_SIZE: usize = 1024 * 1024;
let mut reader = BufReader::new(stream.try_clone()?);
let mut line = String::with_capacity(8192);
reader.read_line(&mut line)?;
if line.len() > MAX_REQUEST_SIZE {
return Err(anyhow!("Request too large: {} bytes", line.len()));
}
let request: Value = serde_json::from_str(line.trim())
.context("Invalid JSON-RPC request")?;
let method = request["method"].as_str()
.ok_or_else(|| anyhow!("Missing method"))?;
let response = match method {
"tools/call" => {
let params = &request["params"];
let tool_name = params["name"].as_str()
.ok_or_else(|| anyhow!("Missing tool name"))?;
let args = params["arguments"].clone();
match mcp.call_tool(tool_name, args) {
Ok(result) => json!({
"jsonrpc": "2.0",
"id": request["id"],
"result": result
}),
Err(e) => json!({
"jsonrpc": "2.0",
"id": request["id"],
"error": {"message": e.to_string()}
}),
}
}
"tools/list" => {
match mcp.list_tools() {
Ok(result) => json!({
"jsonrpc": "2.0",
"id": request["id"],
"result": result
}),
Err(e) => json!({
"jsonrpc": "2.0",
"id": request["id"],
"error": {"message": e.to_string()}
}),
}
}
_ => json!({
"jsonrpc": "2.0",
"id": request["id"],
"error": {"message": format!("Unknown method: {}", method)}
}),
};
let response_str = serde_json::to_string(&response)?;
writeln!(stream, "{}", response_str)?;
Ok(())
}
fn connect_to_daemon(server_name: &str) -> Result<UnixStream> {
let daemon_mgr = DaemonManager::new(server_name);
let socket_path = daemon_mgr.get_socket_path()
.context("Failed to get socket path (daemon not started?)")?;
let stream = UnixStream::connect(&socket_path)
.context("Failed to connect to daemon (is it running?)")?;
stream.set_read_timeout(Some(Duration::from_secs(30)))
.context("Failed to set read timeout")?;
stream.set_write_timeout(Some(Duration::from_secs(30)))
.context("Failed to set write timeout")?;
Ok(stream)
}
fn send_daemon_request(mut stream: UnixStream, request: Value) -> Result<Value> {
let request_str = serde_json::to_string(&request)?;
writeln!(stream, "{}", request_str)?;
let mut reader = BufReader::new(stream);
let mut line = String::new();
reader.read_line(&mut line)?;
let response: Value = serde_json::from_str(line.trim())
.context("Invalid JSON-RPC response")?;
if let Some(error) = response.get("error") {
return Err(anyhow!("Daemon error: {}", error));
}
Ok(response["result"].clone())
}
fn call_via_daemon(server_name: &str, tool: &str, args: Value) -> Result<Value> {
let stream = connect_to_daemon(server_name)?;
let request = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": tool,
"arguments": args
}
});
send_daemon_request(stream, request)
}
fn list_tools_via_daemon(server_name: &str) -> Result<Value> {
let stream = connect_to_daemon(server_name)?;
let request = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
});
send_daemon_request(stream, request)
}
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
if args.len() > 1 && args[1] == "__internal_daemon" {
let server_name = args.iter()
.position(|a| a == "--server")
.and_then(|i| args.get(i + 1))
.ok_or_else(|| anyhow!("__internal_daemon requires --server"))?
.clone();
let extra_args = args.iter()
.position(|a| a == "--server-args")
.and_then(|i| args.get(i + 1))
.and_then(|s| serde_json::from_str::<Vec<String>>(s).ok());
let cli_config = args.iter()
.position(|a| a == "--config" || a == "-c")
.and_then(|i| args.get(i + 1))
.map(PathBuf::from);
let config = load_server_config(cli_config)?;
let profile = config.servers.get(&server_name)
.ok_or_else(|| anyhow!("Server '{}' not found", server_name))?;
return run_daemon(&server_name, profile, extra_args);
}
let filtered_args: Vec<String> = std::env::args()
.filter(|arg| !arg.is_empty())
.collect();
let cli = Cli::parse_from(filtered_args);
match cli.command {
Commands::ListServers => {
let config = load_server_config(cli.config.clone())?;
println!("Configured MCP servers:\n");
for (name, profile) in config.servers {
let desc = if profile.description.is_empty() {
"No description"
} else {
&profile.description
};
println!(" {}: {}", name, desc);
println!(" Command: {:?}", profile.command);
if !profile.default_args.is_empty() {
println!(" Default args: {:?}", profile.default_args);
}
if profile.supports_daemon {
println!(" Daemon support: yes");
}
println!();
}
Ok(())
}
Commands::Call { tool, args } => {
let server_name = cli.server.ok_or_else(|| {
anyhow!("--server required. Use 'list-servers' to see available servers.")
})?;
let config = load_server_config(cli.config.clone())?;
let _profile = config
.servers
.get(&server_name)
.ok_or_else(|| anyhow!("Server '{}' not found in config", server_name))?;
let daemon_mgr = DaemonManager::new(&server_name);
if !daemon_mgr.is_running().unwrap_or(false) {
return Err(daemon_not_running_error(&server_name));
}
let json_str = if args == "-" {
let mut buffer = String::new();
std::io::stdin()
.read_to_string(&mut buffer)
.context("Failed to read JSON from stdin")?;
buffer
} else {
args
};
let args_json: Value =
serde_json::from_str(&json_str).context("Invalid JSON arguments")?;
let result = call_via_daemon(&server_name, &tool, args_json)?;
println!("{}", serde_json::to_string_pretty(&result)?);
Ok(())
}
Commands::ListTools => {
let server_name = cli.server.ok_or_else(|| {
anyhow!("--server required. Use 'list-servers' to see available servers.")
})?;
let config = load_server_config(cli.config.clone())?;
let _profile = config
.servers
.get(&server_name)
.ok_or_else(|| anyhow!("Server '{}' not found in config", server_name))?;
let daemon_mgr = DaemonManager::new(&server_name);
if !daemon_mgr.is_running().unwrap_or(false) {
return Err(daemon_not_running_error(&server_name));
}
let result = list_tools_via_daemon(&server_name)?;
println!("{}", serde_json::to_string_pretty(&result)?);
Ok(())
}
Commands::Shell => {
let server_name = cli.server.ok_or_else(|| {
anyhow!("--server required. Use 'list-servers' to see available servers.")
})?;
let config = load_server_config(cli.config.clone())?;
let _profile = config
.servers
.get(&server_name)
.ok_or_else(|| anyhow!("Server '{}' not found in config", server_name))?;
let daemon_mgr = DaemonManager::new(&server_name);
if !daemon_mgr.is_running().unwrap_or(false) {
return Err(daemon_not_running_error(&server_name));
}
let project = get_project_path();
println!("MCP Shell ({}) - Project: {}", server_name, project);
println!("Commands: call <tool> [json], list-tools, exit");
println!();
loop {
print!("mcp> ");
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let input = input.trim();
if input.is_empty() {
continue;
}
if input == "exit" || input == "quit" {
break;
}
if input == "list-tools" {
match list_tools_via_daemon(&server_name) {
Ok(result) => println!("{}", serde_json::to_string_pretty(&result)?),
Err(e) => eprintln!("Error: {}", e),
}
continue;
}
if let Some(rest) = input.strip_prefix("call ") {
let parts: Vec<&str> = rest.splitn(2, ' ').collect();
if !parts.is_empty() {
let tool = parts[0];
let args = parts.get(1).unwrap_or(&"{}");
match serde_json::from_str(args) {
Ok(args_json) => match call_via_daemon(&server_name, tool, args_json) {
Ok(result) => {
println!("{}", serde_json::to_string_pretty(&result)?)
}
Err(e) => eprintln!("Error: {}", e),
},
Err(e) => eprintln!("Invalid JSON args: {}", e),
}
} else {
eprintln!("Usage: call <tool_name> [json_args]");
}
} else {
eprintln!("Usage: call <tool_name> [json_args] | list-tools | exit");
}
}
println!("Goodbye!");
Ok(())
}
Commands::StartDaemon => {
let server_name = cli.server.ok_or_else(|| {
anyhow!("--server required")
})?;
let config = load_server_config(cli.config.clone())?;
let profile = config
.servers
.get(&server_name)
.ok_or_else(|| anyhow!("Server '{}' not found in config", server_name))?;
let extra_args = if let Some(args_str) = &cli.server_args {
Some(serde_json::from_str::<Vec<String>>(args_str)
.context("Invalid JSON in --server-args")?)
} else {
None
};
let daemon_mgr = DaemonManager::new(&server_name);
daemon_mgr.start(profile, extra_args)?;
Ok(())
}
Commands::StopDaemon => {
let server_name = cli.server.ok_or_else(|| {
anyhow!("--server required")
})?;
let daemon_mgr = DaemonManager::new(&server_name);
daemon_mgr.stop()?;
Ok(())
}
Commands::DaemonStatus => {
let server_name = cli.server.ok_or_else(|| {
anyhow!("--server required")
})?;
let daemon_mgr = DaemonManager::new(&server_name);
daemon_mgr.status()?;
Ok(())
}
}
}