feat(cli): add mcp command surface

This commit is contained in:
Bryan Helmkamp 2026-05-11 07:52:28 -04:00
parent 314f1b041b
commit 88fc2392f5
No known key found for this signature in database
13 changed files with 332 additions and 0 deletions

13
Cargo.lock generated
View file

@ -1679,6 +1679,7 @@ dependencies = [
"fabro-llm",
"fabro-macros",
"fabro-mcp",
"fabro-mcp-server",
"fabro-model",
"fabro-oauth",
"fabro-proc",
@ -2019,6 +2020,18 @@ dependencies = [
"tracing",
]
[[package]]
name = "fabro-mcp-server"
version = "0.230.0-nightly.0"
dependencies = [
"anyhow",
"dirs",
"rmcp",
"serde",
"serde_json",
"tokio",
]
[[package]]
name = "fabro-model"
version = "0.230.0-nightly.0"

View file

@ -31,6 +31,7 @@ fabro-hooks = { path = "../fabro-hooks" }
fabro-install = { path = "../fabro-install" }
fabro-interview = { path = "../fabro-interview" }
fabro-mcp = { path = "../fabro-mcp" }
fabro-mcp-server = { path = "../fabro-mcp-server" }
fabro-proc = { path = "../fabro-proc" }
fabro-sandbox = { path = "../fabro-sandbox", features = ["daytona"] }
fabro-checkpoint = { path = "../fabro-checkpoint" }

View file

@ -168,6 +168,49 @@ pub(crate) struct ServerConnectionArgs {
pub(crate) target: ServerTargetArgs,
}
#[derive(Args)]
pub(crate) struct McpNamespace {
#[command(subcommand)]
pub(crate) command: McpCommand,
}
#[derive(Subcommand)]
pub(crate) enum McpCommand {
/// Start the Fabro MCP server over stdio
Start(McpStartArgs),
/// Print MCP client configuration JSON
Config(McpConfigArgs),
/// Configure an MCP client to launch Fabro
Init(McpInitArgs),
}
#[derive(Args, Debug, Clone, Default)]
pub(crate) struct McpStartArgs {
#[command(flatten)]
pub(crate) connection: ServerConnectionArgs,
}
#[derive(Args, Debug, Clone, Default)]
pub(crate) struct McpConfigArgs {
#[command(flatten)]
pub(crate) connection: ServerConnectionArgs,
}
#[derive(Args, Debug, Clone)]
pub(crate) struct McpInitArgs {
pub(crate) agent: McpAgent,
#[command(flatten)]
pub(crate) connection: ServerConnectionArgs,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub(crate) enum McpAgent {
Claude,
Cursor,
Windsurf,
}
#[derive(Args, Debug, Clone, Default)]
pub(crate) struct InputOverrideArgs {
/// Override a workflow input value (repeatable, format: KEY=VALUE)
@ -1118,6 +1161,8 @@ pub(crate) enum Commands {
#[command(subcommand)]
command: Option<ModelsCommand>,
},
/// Model Context Protocol server
Mcp(McpNamespace),
/// Server operations
Server(ServerNamespace),
/// Check environment and integration health
@ -1209,6 +1254,11 @@ impl Commands {
Some(ModelsCommand::Test(_)) => "model test",
None => "model",
},
Self::Mcp(ns) => match &ns.command {
McpCommand::Start(_) => "mcp start",
McpCommand::Config(_) => "mcp config",
McpCommand::Init(_) => "mcp init",
},
Self::Server(ns) => match &ns.command {
ServerCommand::Start(_) => "server start",
ServerCommand::Stop(_) => "server stop",

View file

@ -0,0 +1,66 @@
use anyhow::{Context as _, Result};
use crate::args::{McpAgent, McpCommand, McpNamespace, ServerConnectionArgs};
use crate::command_context::CommandContext;
pub(crate) async fn dispatch(ns: McpNamespace, base_ctx: &CommandContext) -> Result<()> {
match ns.command {
McpCommand::Start(args) => {
fabro_mcp_server::start(server_settings(base_ctx, &args.connection)?).await
}
McpCommand::Config(args) => {
let json = fabro_mcp_server::config_json(config_settings(&args.connection));
print!("{json}");
Ok(())
}
McpCommand::Init(args) => {
fabro_mcp_server::init_agent(init_settings(args.agent, &args.connection)?)?;
Ok(())
}
}
}
fn server_settings(
base_ctx: &CommandContext,
connection: &ServerConnectionArgs,
) -> Result<fabro_mcp_server::McpServerSettings> {
Ok(fabro_mcp_server::McpServerSettings {
config: config_settings(connection),
home_dir: home_dir()?,
cwd: base_ctx.cwd().to_path_buf(),
})
}
fn init_settings(
agent: McpAgent,
connection: &ServerConnectionArgs,
) -> Result<fabro_mcp_server::McpInitSettings> {
Ok(fabro_mcp_server::McpInitSettings {
agent: McpAgentForServer(agent).into(),
config: config_settings(connection),
home_dir: home_dir()?,
})
}
fn config_settings(connection: &ServerConnectionArgs) -> fabro_mcp_server::McpConfigSettings {
fabro_mcp_server::McpConfigSettings {
server: connection.target.server.clone(),
storage_dir: connection.storage_dir.clone_path(),
}
}
fn home_dir() -> Result<std::path::PathBuf> {
dirs::home_dir().context("failed to resolve home directory for MCP config")
}
struct McpAgentForServer(McpAgent);
impl From<McpAgentForServer> for fabro_mcp_server::McpAgent {
fn from(value: McpAgentForServer) -> Self {
match value.0 {
McpAgent::Claude => Self::Claude,
McpAgent::Cursor => Self::Cursor,
McpAgent::Windsurf => Self::Windsurf,
}
}
}

View file

@ -7,6 +7,7 @@ pub(crate) mod dump;
pub(crate) mod exec;
pub(crate) mod graph;
pub(crate) mod install;
pub(crate) mod mcp;
pub(crate) mod model;
pub(crate) mod parse;
pub(crate) mod pr;

View file

@ -281,6 +281,9 @@ async fn main_inner(worker_token: Option<String>) -> (String, Result<()>) {
Commands::Model { command } => {
commands::model::execute(command, &base_ctx).await?;
}
Commands::Mcp(ns) => {
commands::mcp::dispatch(ns, &base_ctx).await?;
}
Commands::Server(ns) => {
Box::pin(commands::server::dispatch(
ns.command,

View file

@ -0,0 +1,112 @@
use fabro_test::{fabro_snapshot, test_context};
#[test]
fn help() {
let context = test_context!();
let mut cmd = context.command();
cmd.args(["mcp", "--help"]);
fabro_snapshot!(context.filters(), cmd, @"
success: true
exit_code: 0
----- stdout -----
Model Context Protocol server
Usage: fabro mcp [OPTIONS] <COMMAND>
Commands:
start Start the Fabro MCP server over stdio
config Print MCP client configuration JSON
init Configure an MCP client to launch Fabro
help Print this message or the help of the given subcommand(s)
Options:
--json Output as JSON [env: FABRO_JSON=]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--verbose Enable verbose output [env: FABRO_VERBOSE=]
-h, --help Print help
----- stderr -----
");
}
#[test]
fn start_help() {
let context = test_context!();
let mut cmd = context.command();
cmd.args(["mcp", "start", "--help"]);
fabro_snapshot!(context.filters(), cmd, @"
success: true
exit_code: 0
----- stdout -----
Start the Fabro MCP server over stdio
Usage: fabro mcp start [OPTIONS]
Options:
--json Output as JSON [env: FABRO_JSON=]
--storage-dir <STORAGE_DIR> Local storage directory (default: ~/.fabro/storage) [env: FABRO_STORAGE_DIR=]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--server <SERVER> Fabro server target: http(s) URL or absolute Unix socket path [env: FABRO_SERVER=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--verbose Enable verbose output [env: FABRO_VERBOSE=]
-h, --help Print help
----- stderr -----
");
}
#[test]
fn config_help() {
let context = test_context!();
let mut cmd = context.command();
cmd.args(["mcp", "config", "--help"]);
fabro_snapshot!(context.filters(), cmd, @"
success: true
exit_code: 0
----- stdout -----
Print MCP client configuration JSON
Usage: fabro mcp config [OPTIONS]
Options:
--json Output as JSON [env: FABRO_JSON=]
--storage-dir <STORAGE_DIR> Local storage directory (default: ~/.fabro/storage) [env: FABRO_STORAGE_DIR=]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--server <SERVER> Fabro server target: http(s) URL or absolute Unix socket path [env: FABRO_SERVER=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--verbose Enable verbose output [env: FABRO_VERBOSE=]
-h, --help Print help
----- stderr -----
");
}
#[test]
fn init_help() {
let context = test_context!();
let mut cmd = context.command();
cmd.args(["mcp", "init", "--help"]);
fabro_snapshot!(context.filters(), cmd, @"
success: true
exit_code: 0
----- stdout -----
Configure an MCP client to launch Fabro
Usage: fabro mcp init [OPTIONS] <AGENT>
Arguments:
<AGENT> [possible values: claude, cursor, windsurf]
Options:
--json Output as JSON [env: FABRO_JSON=]
--storage-dir <STORAGE_DIR> Local storage directory (default: ~/.fabro/storage) [env: FABRO_STORAGE_DIR=]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--server <SERVER> Fabro server target: http(s) URL or absolute Unix socket path [env: FABRO_SERVER=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--verbose Enable verbose output [env: FABRO_VERBOSE=]
-h, --help Print help
----- stderr -----
");
}

View file

@ -20,6 +20,7 @@ mod inspect;
mod install;
mod json_global;
mod logs;
mod mcp;
mod model;
mod model_list;
mod model_test;

View file

@ -0,0 +1,21 @@
[package]
name = "fabro-mcp-server"
edition.workspace = true
version.workspace = true
publish = false
license.workspace = true
description = "Fabro MCP stdio server"
[lib]
doctest = false
[lints]
workspace = true
[dependencies]
anyhow.workspace = true
dirs.workspace = true
rmcp = { workspace = true, features = ["server", "macros", "schemars", "transport-io"] }
serde.workspace = true
serde_json.workspace = true
tokio.workspace = true

View file

@ -0,0 +1,21 @@
use anyhow::Result;
use serde_json::json;
use crate::{McpConfigSettings, McpInitSettings};
pub fn config_json(_settings: McpConfigSettings) -> String {
serde_json::to_string_pretty(&json!({
"mcpServers": {
"fabro": {
"command": "fabro",
"args": ["mcp", "start"]
}
}
}))
.expect("static MCP config should serialize")
+ "\n"
}
pub fn init_agent(_settings: McpInitSettings) -> Result<()> {
Ok(())
}

View file

@ -0,0 +1,35 @@
mod config;
mod run_tools;
mod server;
use std::path::PathBuf;
pub use config::{config_json, init_agent};
pub use server::start;
#[derive(Debug, Clone)]
pub struct McpServerSettings {
pub config: McpConfigSettings,
pub home_dir: PathBuf,
pub cwd: PathBuf,
}
#[derive(Debug, Clone, Default)]
pub struct McpConfigSettings {
pub server: Option<String>,
pub storage_dir: Option<PathBuf>,
}
#[derive(Debug, Clone)]
pub struct McpInitSettings {
pub agent: McpAgent,
pub config: McpConfigSettings,
pub home_dir: PathBuf,
}
#[derive(Debug, Clone, Copy)]
pub enum McpAgent {
Claude,
Cursor,
Windsurf,
}

View file

@ -0,0 +1 @@
// Run-management MCP tools are implemented after the stdio server skeleton.

View file

@ -0,0 +1,7 @@
use anyhow::{Result, bail};
use crate::McpServerSettings;
pub async fn start(_settings: McpServerSettings) -> Result<()> {
bail!("fabro mcp start is not implemented yet")
}