mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-07 08:27:12 +00:00
feat(cli): add mcp command surface
This commit is contained in:
parent
314f1b041b
commit
88fc2392f5
13 changed files with 332 additions and 0 deletions
13
Cargo.lock
generated
13
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
66
lib/crates/fabro-cli/src/commands/mcp/mod.rs
Normal file
66
lib/crates/fabro-cli/src/commands/mcp/mod.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
112
lib/crates/fabro-cli/tests/it/cmd/mcp.rs
Normal file
112
lib/crates/fabro-cli/tests/it/cmd/mcp.rs
Normal 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 -----
|
||||
");
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ mod inspect;
|
|||
mod install;
|
||||
mod json_global;
|
||||
mod logs;
|
||||
mod mcp;
|
||||
mod model;
|
||||
mod model_list;
|
||||
mod model_test;
|
||||
|
|
|
|||
21
lib/crates/fabro-mcp-server/Cargo.toml
Normal file
21
lib/crates/fabro-mcp-server/Cargo.toml
Normal 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
|
||||
21
lib/crates/fabro-mcp-server/src/config.rs
Normal file
21
lib/crates/fabro-mcp-server/src/config.rs
Normal 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(())
|
||||
}
|
||||
35
lib/crates/fabro-mcp-server/src/lib.rs
Normal file
35
lib/crates/fabro-mcp-server/src/lib.rs
Normal 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,
|
||||
}
|
||||
1
lib/crates/fabro-mcp-server/src/run_tools.rs
Normal file
1
lib/crates/fabro-mcp-server/src/run_tools.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
// Run-management MCP tools are implemented after the stdio server skeleton.
|
||||
7
lib/crates/fabro-mcp-server/src/server.rs
Normal file
7
lib/crates/fabro-mcp-server/src/server.rs
Normal 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")
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue