offline-cli 0.3.3

The Offline Protocol developer CLI
//! `offline mcp`: serve the registry over MCP.

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 {
    /// Run the MCP server over stdio (newline-delimited JSON-RPC)
    Serve,
    /// Print setup instructions/config for popular MCP clients
    Install(InstallArgs),
    /// List the MCP tools this server exposes
    Tools(ToolsArgs),
}

#[derive(Args)]
pub struct InstallArgs {
    /// Which client to print configuration for
    #[arg(value_enum, default_value = "claude-code")]
    pub client: Client,
}

#[derive(Args)]
pub struct ToolsArgs {
    /// Output JSON
    #[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();

            // Log on stderr; stdout is JSON-RPC.
            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(())
}