mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
Merge pull request #810 from fabro-sh/mcp-config-name
Add --name to fabro mcp config and fabro mcp init
This commit is contained in:
commit
6ac769c4cd
7 changed files with 183 additions and 32 deletions
|
|
@ -28,6 +28,15 @@ fabro mcp start
|
|||
|
||||
Pass `--server` when the MCP client should connect to a specific Fabro server, or `--storage-dir` when it should use a non-default CLI storage directory.
|
||||
|
||||
Both commands register the entry under the `mcpServers` key `fabro` by default. Pass `--name` to choose a different key. Each named entry launches its own single-target `fabro mcp start` process, so you can register more than one Fabro server in the same MCP client:
|
||||
|
||||
```bash
|
||||
fabro mcp init claude --name fabro-production --server https://fabro.example.com
|
||||
fabro mcp init claude --name fabro-testing --server https://fabro-testing.example.com
|
||||
```
|
||||
|
||||
`fabro mcp init` keeps entries with other names and replaces only the entry that matches `--name`.
|
||||
|
||||
| Tool | Purpose |
|
||||
|---|---|
|
||||
| `fabro_run_create` | Create one or more workflow runs, optionally under a parent run, starting them by default. |
|
||||
|
|
|
|||
|
|
@ -611,6 +611,7 @@ fabro mcp config [OPTIONS]
|
|||
|
||||
| Option | Description |
|
||||
| --- | --- |
|
||||
| `--name <name>` | Name of the mcpServers entry; use distinct names to register multiple Fabro servers<br />Default: `fabro` |
|
||||
| `--server <server>` | Fabro server target: http(s) URL or absolute Unix socket path |
|
||||
| `--storage-dir <storage_dir>` | Local storage directory (default: ~/.fabro/storage) |
|
||||
|
||||
|
|
@ -632,6 +633,7 @@ fabro mcp init [OPTIONS] <AGENT>
|
|||
|
||||
| Option | Description |
|
||||
| --- | --- |
|
||||
| `--name <name>` | Name of the mcpServers entry; use distinct names to register multiple Fabro servers<br />Default: `fabro` |
|
||||
| `--server <server>` | Fabro server target: http(s) URL or absolute Unix socket path |
|
||||
| `--storage-dir <storage_dir>` | Local storage directory (default: ~/.fabro/storage) |
|
||||
|
||||
|
|
|
|||
|
|
@ -191,8 +191,13 @@ pub(crate) struct McpStartArgs {
|
|||
pub(crate) connection: ServerConnectionArgs,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug, Clone, Default)]
|
||||
#[derive(Args, Debug, Clone)]
|
||||
pub(crate) struct McpConfigArgs {
|
||||
/// Name of the mcpServers entry; use distinct names to register multiple
|
||||
/// Fabro servers
|
||||
#[arg(long, value_name = "NAME", default_value = fabro_mcp_server::SERVER_NAME, value_parser = clap::builder::NonEmptyStringValueParser::new())]
|
||||
pub(crate) name: String,
|
||||
|
||||
#[command(flatten)]
|
||||
pub(crate) connection: ServerConnectionArgs,
|
||||
}
|
||||
|
|
@ -202,7 +207,7 @@ pub(crate) struct McpInitArgs {
|
|||
pub(crate) agent: McpAgent,
|
||||
|
||||
#[command(flatten)]
|
||||
pub(crate) connection: ServerConnectionArgs,
|
||||
pub(crate) config: McpConfigArgs,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ use std::fmt::Write as _;
|
|||
|
||||
use anyhow::{Context as _, Result};
|
||||
|
||||
use crate::args::{McpAgent, McpCommand, McpNamespace, ServerConnectionArgs};
|
||||
use crate::args::{
|
||||
McpAgent, McpCommand, McpConfigArgs, McpInitArgs, McpNamespace, ServerConnectionArgs,
|
||||
};
|
||||
use crate::command_context::CommandContext;
|
||||
use crate::server_client;
|
||||
|
||||
|
|
@ -12,12 +14,12 @@ pub(crate) async fn dispatch(ns: McpNamespace, base_ctx: &CommandContext) -> Res
|
|||
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))?;
|
||||
let json = fabro_mcp_server::config_json(&config_settings(&args))?;
|
||||
let _ = write!(base_ctx.printer().stdout_important(), "{json}");
|
||||
Ok(())
|
||||
}
|
||||
McpCommand::Init(args) => {
|
||||
fabro_mcp_server::init_agent(&init_settings(args.agent, &args.connection)?)?;
|
||||
fabro_mcp_server::init_agent(&init_settings(&args)?)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -56,21 +58,19 @@ fn server_settings(
|
|||
})
|
||||
}
|
||||
|
||||
fn init_settings(
|
||||
agent: McpAgent,
|
||||
connection: &ServerConnectionArgs,
|
||||
) -> Result<fabro_mcp_server::McpInitSettings> {
|
||||
fn init_settings(args: &McpInitArgs) -> Result<fabro_mcp_server::McpInitSettings> {
|
||||
Ok(fabro_mcp_server::McpInitSettings {
|
||||
agent: McpAgentForServer(agent).into(),
|
||||
config: config_settings(connection),
|
||||
agent: McpAgentForServer(args.agent).into(),
|
||||
config: config_settings(&args.config),
|
||||
home_dir: home_dir()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn config_settings(connection: &ServerConnectionArgs) -> fabro_mcp_server::McpConfigSettings {
|
||||
fn config_settings(args: &McpConfigArgs) -> fabro_mcp_server::McpConfigSettings {
|
||||
fabro_mcp_server::McpConfigSettings {
|
||||
server: connection.target.server.clone(),
|
||||
storage_dir: connection.storage_dir.clone_path(),
|
||||
name: args.name.clone(),
|
||||
server: args.connection.target.server.clone(),
|
||||
storage_dir: args.connection.storage_dir.clone_path(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -122,10 +122,11 @@ fn config_help() {
|
|||
|
||||
Options:
|
||||
--json Output as JSON [env: FABRO_JSON=]
|
||||
--storage-dir <STORAGE_DIR> Local storage directory (default: ~/.fabro/storage) [env: FABRO_STORAGE_DIR=]
|
||||
--name <NAME> Name of the mcpServers entry; use distinct names to register multiple Fabro servers [default: fabro]
|
||||
--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=]
|
||||
--storage-dir <STORAGE_DIR> Local storage directory (default: ~/.fabro/storage) [env: FABRO_STORAGE_DIR=]
|
||||
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
|
||||
--server <SERVER> Fabro server target: http(s) URL or absolute Unix socket path [env: FABRO_SERVER=]
|
||||
--quiet Suppress non-essential output [env: FABRO_QUIET=]
|
||||
--verbose Enable verbose output [env: FABRO_VERBOSE=]
|
||||
-h, --help Print help
|
||||
|
|
@ -151,10 +152,11 @@ fn init_help() {
|
|||
|
||||
Options:
|
||||
--json Output as JSON [env: FABRO_JSON=]
|
||||
--storage-dir <STORAGE_DIR> Local storage directory (default: ~/.fabro/storage) [env: FABRO_STORAGE_DIR=]
|
||||
--name <NAME> Name of the mcpServers entry; use distinct names to register multiple Fabro servers [default: fabro]
|
||||
--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=]
|
||||
--storage-dir <STORAGE_DIR> Local storage directory (default: ~/.fabro/storage) [env: FABRO_STORAGE_DIR=]
|
||||
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
|
||||
--server <SERVER> Fabro server target: http(s) URL or absolute Unix socket path [env: FABRO_SERVER=]
|
||||
--quiet Suppress non-essential output [env: FABRO_QUIET=]
|
||||
--verbose Enable verbose output [env: FABRO_VERBOSE=]
|
||||
-h, --help Print help
|
||||
|
|
@ -221,6 +223,55 @@ fn config_preserves_connection_flags() {
|
|||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_uses_custom_entry_name() {
|
||||
let context = test_context!();
|
||||
let mut cmd = context.command();
|
||||
cmd.args([
|
||||
"mcp",
|
||||
"config",
|
||||
"--name",
|
||||
"fabro-production",
|
||||
"--server",
|
||||
"https://fabro.example.test",
|
||||
]);
|
||||
fabro_snapshot!(context.filters(), cmd, @r#"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
{
|
||||
"mcpServers": {
|
||||
"fabro-production": {
|
||||
"command": "fabro",
|
||||
"args": [
|
||||
"mcp",
|
||||
"start",
|
||||
"--server",
|
||||
"https://fabro.example.test"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
----- stderr -----
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_rejects_empty_entry_name() {
|
||||
let context = test_context!();
|
||||
let mut cmd = context.command();
|
||||
cmd.args(["mcp", "config", "--name", ""]);
|
||||
fabro_snapshot!(context.filters(), cmd, @"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: a value is required for '--name <NAME>' but none was supplied
|
||||
|
||||
For more information, try '--help'.
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init_cursor_writes_idempotent_config() {
|
||||
let context = test_context!();
|
||||
|
|
@ -417,6 +468,91 @@ fn init_preserves_existing_servers() {
|
|||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init_merges_multiple_named_fabro_entries() {
|
||||
let context = test_context!();
|
||||
context
|
||||
.command()
|
||||
.args(["mcp", "init", "cursor"])
|
||||
.assert()
|
||||
.success();
|
||||
context
|
||||
.command()
|
||||
.args([
|
||||
"mcp",
|
||||
"init",
|
||||
"cursor",
|
||||
"--name",
|
||||
"fabro-production",
|
||||
"--server",
|
||||
"https://production.example.test",
|
||||
])
|
||||
.assert()
|
||||
.success();
|
||||
context
|
||||
.command()
|
||||
.args([
|
||||
"mcp",
|
||||
"init",
|
||||
"cursor",
|
||||
"--name",
|
||||
"fabro-testing",
|
||||
"--server",
|
||||
"https://testing.example.test",
|
||||
])
|
||||
.assert()
|
||||
.success();
|
||||
// Reusing a name updates only that entry.
|
||||
context
|
||||
.command()
|
||||
.args([
|
||||
"mcp",
|
||||
"init",
|
||||
"cursor",
|
||||
"--name",
|
||||
"fabro-production",
|
||||
"--server",
|
||||
"https://production.example.test:8443",
|
||||
])
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let config_path = context.home_dir.join(".cursor").join("mcp.json");
|
||||
let config: serde_json::Value =
|
||||
serde_json::from_str(&std::fs::read_to_string(config_path).unwrap()).unwrap();
|
||||
fabro_json_snapshot!(context, config, @r#"
|
||||
{
|
||||
"mcpServers": {
|
||||
"fabro": {
|
||||
"command": "fabro",
|
||||
"args": [
|
||||
"mcp",
|
||||
"start"
|
||||
]
|
||||
},
|
||||
"fabro-production": {
|
||||
"command": "fabro",
|
||||
"args": [
|
||||
"mcp",
|
||||
"start",
|
||||
"--server",
|
||||
"https://production.example.test:8443"
|
||||
]
|
||||
},
|
||||
"fabro-testing": {
|
||||
"command": "fabro",
|
||||
"args": [
|
||||
"mcp",
|
||||
"start",
|
||||
"--server",
|
||||
"https://testing.example.test"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init_invalid_json_fails_without_overwrite() {
|
||||
let context = test_context!();
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use anyhow::{Context as _, Result, anyhow};
|
|||
use serde_json::map::Entry;
|
||||
use serde_json::{Map, Value, json};
|
||||
|
||||
use crate::{McpAgent, McpConfigSettings, McpInitSettings, SERVER_NAME};
|
||||
use crate::{McpAgent, McpConfigSettings, McpInitSettings};
|
||||
|
||||
pub fn config_json(settings: &McpConfigSettings) -> Result<String> {
|
||||
serde_json::to_string_pretty(&generic_config(settings))
|
||||
|
|
@ -18,19 +18,16 @@ pub fn config_json(settings: &McpConfigSettings) -> Result<String> {
|
|||
}
|
||||
|
||||
pub fn init_agent(settings: &McpInitSettings) -> Result<()> {
|
||||
let entry = server_entry(&settings.config);
|
||||
for path in agent_config_paths(settings.agent, &settings.home_dir) {
|
||||
merge_server_entry(&path, entry.clone())?;
|
||||
merge_server_entry(&path, &settings.config)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generic_config(settings: &McpConfigSettings) -> Value {
|
||||
json!({
|
||||
"mcpServers": {
|
||||
SERVER_NAME: server_entry(settings)
|
||||
}
|
||||
})
|
||||
let mut servers = Map::new();
|
||||
servers.insert(settings.name.clone(), server_entry(settings));
|
||||
json!({ "mcpServers": servers })
|
||||
}
|
||||
|
||||
fn server_entry(settings: &McpConfigSettings) -> Value {
|
||||
|
|
@ -53,7 +50,7 @@ fn start_args(settings: &McpConfigSettings) -> Vec<String> {
|
|||
args
|
||||
}
|
||||
|
||||
fn merge_server_entry(path: &Path, entry: Value) -> Result<()> {
|
||||
fn merge_server_entry(path: &Path, settings: &McpConfigSettings) -> Result<()> {
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.with_context(|| format!("failed to create {}", parent.display()))?;
|
||||
|
|
@ -80,7 +77,7 @@ fn merge_server_entry(path: &Path, entry: Value) -> Result<()> {
|
|||
path.display()
|
||||
)
|
||||
})?;
|
||||
servers_object.insert(SERVER_NAME.to_string(), entry);
|
||||
servers_object.insert(settings.name.clone(), server_entry(settings));
|
||||
|
||||
let rendered = serde_json::to_string_pretty(&root)
|
||||
.map(|json| format!("{json}\n"))
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ pub use config::{config_json, init_agent};
|
|||
use fabro_client::Client;
|
||||
pub use server::start;
|
||||
|
||||
/// The name this MCP server reports over the wire and registers under in agent
|
||||
/// config files.
|
||||
pub(crate) const SERVER_NAME: &str = "fabro";
|
||||
/// The name this MCP server reports over the wire. It is also the default
|
||||
/// `mcpServers` key that `fabro mcp config` and `fabro mcp init` register.
|
||||
pub const SERVER_NAME: &str = "fabro";
|
||||
|
||||
pub type FabroClientFuture = Pin<Box<dyn Future<Output = Result<Client>> + Send>>;
|
||||
|
||||
|
|
@ -39,8 +39,10 @@ impl std::fmt::Debug for FabroMcpServerSettings {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct McpConfigSettings {
|
||||
/// The `mcpServers` key the generated client entry is registered under.
|
||||
pub name: String,
|
||||
pub server: Option<String>,
|
||||
pub storage_dir: Option<PathBuf>,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue