use anyhow::Result;
use clap::{Args, Subcommand, ValueEnum};
use offline_mcp::McpServer;
use offline_registry::Registry;
use serde_json::json;
use crate::output;
#[derive(Subcommand)]
pub enum McpCommand {
Serve,
Install(InstallArgs),
Tools(ToolsArgs),
}
#[derive(Args)]
pub struct InstallArgs {
#[arg(value_enum, default_value = "claude-code")]
pub client: Client,
}
#[derive(Args)]
pub struct ToolsArgs {
#[arg(long)]
pub json: bool,
}
#[derive(Clone, Copy, ValueEnum)]
pub enum Client {
ClaudeCode,
ClaudeDesktop,
Codex,
Cursor,
Vscode,
}
pub fn run(cmd: McpCommand) -> Result<()> {
match cmd {
McpCommand::Serve => {
let registry = Registry::load_default()?;
let packages = registry.packages().count();
let skills = registry.skills().count();
let workflows = registry.workflows().count();
let tools = offline_mcp::tools::tool_definitions().len()
+ crate::setup::mcp_tools::definitions().len();
eprintln!(
"{} MCP server ready (stdio)",
console::style("✓").green().bold()
);
eprintln!(
" {}",
console::style(format!(
"offline-protocol v{} — {packages} packages, {skills} skills, {workflows} workflows, {tools} tools",
env!("CARGO_PKG_VERSION")
))
.dim()
);
let server = McpServer::new(registry)
.with_account_tools(Box::new(crate::setup::mcp_tools::CliAccountTools));
let stdin = std::io::stdin();
let stdout = std::io::stdout();
server.run(stdin.lock(), stdout.lock())?;
eprintln!(
"{} MCP server stopped (stdio closed)",
console::style("✓").green().bold()
);
Ok(())
}
McpCommand::Install(args) => install(args),
McpCommand::Tools(args) => {
let mut defs = offline_mcp::tools::tool_definitions();
defs.extend(crate::setup::mcp_tools::definitions());
if args.json {
return output::json(&defs);
}
output::heading("MCP tools");
for d in defs {
output::item(
d["name"].as_str().unwrap_or("?"),
d["description"].as_str().unwrap_or(""),
);
}
Ok(())
}
}
}
fn install(args: InstallArgs) -> Result<()> {
let server_json = serde_json::to_string_pretty(&json!({
"mcpServers": {
"offline-protocol": {"command": "offline", "args": ["mcp", "serve"]}
}
}))?;
match args.client {
Client::ClaudeCode => {
output::heading("Claude Code");
println!(" claude mcp add offline-protocol -- offline mcp serve");
println!();
println!(" # or per-project: commit this as .mcp.json");
println!("{server_json}");
}
Client::Codex => {
output::heading("Codex");
println!(" codex mcp add offline-protocol -- offline mcp serve");
println!();
println!(" # or add to ~/.codex/config.toml (or .codex/config.toml in a project):");
println!(
"[mcp_servers.offline-protocol]\ncommand = \"offline\"\nargs = [\"mcp\", \"serve\"]"
);
}
Client::ClaudeDesktop => {
output::heading("Claude Desktop");
println!(" Add to claude_desktop_config.json (Settings → Developer → Edit Config):");
println!("{server_json}");
}
Client::Cursor => {
output::heading("Cursor");
println!(" Add to .cursor/mcp.json in your project (or ~/.cursor/mcp.json):");
println!("{server_json}");
}
Client::Vscode => {
output::heading("VS Code");
println!(" Add to .vscode/mcp.json:");
let vscode = serde_json::to_string_pretty(&json!({
"servers": {
"offline-protocol": {
"type": "stdio",
"command": "offline",
"args": ["mcp", "serve"]
}
}
}))?;
println!("{vscode}");
}
}
println!();
output::note("the `offline` binary must be on PATH for the client to spawn it");
Ok(())
}