mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
Add --name to fabro mcp config and fabro mcp init
Both commands always registered the MCP client entry under the fixed `mcpServers` key `fabro`, so users could not register separate Fabro servers (for example production and testing) without editing the client JSON by hand. `--name <NAME>` now selects the `mcpServers` key. It defaults to `fabro` for backward compatibility and rejects empty values. `fabro mcp init` upserts only the named entry and preserves entries with other names, so reusing a name updates that entry in place. Closes #808 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
c90d195c2f
commit
24165b10f5
7 changed files with 187 additions and 28 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.
|
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 |
|
| Tool | Purpose |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `fabro_run_create` | Create one or more workflow runs, optionally under a parent run, starting them by default. |
|
| `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 |
|
| Option | Description |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|
| `--name <name>` | Name of the mcpServers entry; use distinct names to register multiple Fabro servers (default: fabro) |
|
||||||
| `--server <server>` | Fabro server target: http(s) URL or absolute Unix socket path |
|
| `--server <server>` | Fabro server target: http(s) URL or absolute Unix socket path |
|
||||||
| `--storage-dir <storage_dir>` | Local storage directory (default: ~/.fabro/storage) |
|
| `--storage-dir <storage_dir>` | Local storage directory (default: ~/.fabro/storage) |
|
||||||
|
|
||||||
|
|
@ -632,6 +633,7 @@ fabro mcp init [OPTIONS] <AGENT>
|
||||||
|
|
||||||
| Option | Description |
|
| Option | Description |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|
| `--name <name>` | Name of the mcpServers entry; use distinct names to register multiple Fabro servers (default: fabro) |
|
||||||
| `--server <server>` | Fabro server target: http(s) URL or absolute Unix socket path |
|
| `--server <server>` | Fabro server target: http(s) URL or absolute Unix socket path |
|
||||||
| `--storage-dir <storage_dir>` | Local storage directory (default: ~/.fabro/storage) |
|
| `--storage-dir <storage_dir>` | Local storage directory (default: ~/.fabro/storage) |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -191,8 +191,13 @@ pub(crate) struct McpStartArgs {
|
||||||
pub(crate) connection: ServerConnectionArgs,
|
pub(crate) connection: ServerConnectionArgs,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args, Debug, Clone, Default)]
|
#[derive(Args, Debug, Clone)]
|
||||||
pub(crate) struct McpConfigArgs {
|
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)]
|
#[command(flatten)]
|
||||||
pub(crate) connection: ServerConnectionArgs,
|
pub(crate) connection: ServerConnectionArgs,
|
||||||
}
|
}
|
||||||
|
|
@ -201,6 +206,11 @@ pub(crate) struct McpConfigArgs {
|
||||||
pub(crate) struct McpInitArgs {
|
pub(crate) struct McpInitArgs {
|
||||||
pub(crate) agent: McpAgent,
|
pub(crate) agent: McpAgent,
|
||||||
|
|
||||||
|
/// 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)]
|
#[command(flatten)]
|
||||||
pub(crate) connection: ServerConnectionArgs,
|
pub(crate) connection: ServerConnectionArgs,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use std::fmt::Write as _;
|
||||||
|
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
|
|
||||||
use crate::args::{McpAgent, McpCommand, McpNamespace, ServerConnectionArgs};
|
use crate::args::{McpAgent, McpCommand, McpInitArgs, McpNamespace, ServerConnectionArgs};
|
||||||
use crate::command_context::CommandContext;
|
use crate::command_context::CommandContext;
|
||||||
use crate::server_client;
|
use crate::server_client;
|
||||||
|
|
||||||
|
|
@ -12,12 +12,13 @@ pub(crate) async fn dispatch(ns: McpNamespace, base_ctx: &CommandContext) -> Res
|
||||||
fabro_mcp_server::start(server_settings(base_ctx, &args.connection)?).await
|
fabro_mcp_server::start(server_settings(base_ctx, &args.connection)?).await
|
||||||
}
|
}
|
||||||
McpCommand::Config(args) => {
|
McpCommand::Config(args) => {
|
||||||
let json = fabro_mcp_server::config_json(&config_settings(&args.connection))?;
|
let json =
|
||||||
|
fabro_mcp_server::config_json(&config_settings(&args.name, &args.connection))?;
|
||||||
let _ = write!(base_ctx.printer().stdout_important(), "{json}");
|
let _ = write!(base_ctx.printer().stdout_important(), "{json}");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
McpCommand::Init(args) => {
|
McpCommand::Init(args) => {
|
||||||
fabro_mcp_server::init_agent(&init_settings(args.agent, &args.connection)?)?;
|
fabro_mcp_server::init_agent(&init_settings(&args)?)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -56,19 +57,20 @@ fn server_settings(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_settings(
|
fn init_settings(args: &McpInitArgs) -> Result<fabro_mcp_server::McpInitSettings> {
|
||||||
agent: McpAgent,
|
|
||||||
connection: &ServerConnectionArgs,
|
|
||||||
) -> Result<fabro_mcp_server::McpInitSettings> {
|
|
||||||
Ok(fabro_mcp_server::McpInitSettings {
|
Ok(fabro_mcp_server::McpInitSettings {
|
||||||
agent: McpAgentForServer(agent).into(),
|
agent: McpAgentForServer(args.agent).into(),
|
||||||
config: config_settings(connection),
|
config: config_settings(&args.name, &args.connection),
|
||||||
home_dir: home_dir()?,
|
home_dir: home_dir()?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn config_settings(connection: &ServerConnectionArgs) -> fabro_mcp_server::McpConfigSettings {
|
fn config_settings(
|
||||||
|
name: &str,
|
||||||
|
connection: &ServerConnectionArgs,
|
||||||
|
) -> fabro_mcp_server::McpConfigSettings {
|
||||||
fabro_mcp_server::McpConfigSettings {
|
fabro_mcp_server::McpConfigSettings {
|
||||||
|
name: name.to_string(),
|
||||||
server: connection.target.server.clone(),
|
server: connection.target.server.clone(),
|
||||||
storage_dir: connection.storage_dir.clone_path(),
|
storage_dir: connection.storage_dir.clone_path(),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -122,10 +122,11 @@ fn config_help() {
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--json Output as JSON [env: FABRO_JSON=]
|
--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=]
|
--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]
|
--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=]
|
--quiet Suppress non-essential output [env: FABRO_QUIET=]
|
||||||
--verbose Enable verbose output [env: FABRO_VERBOSE=]
|
--verbose Enable verbose output [env: FABRO_VERBOSE=]
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
@ -151,10 +152,11 @@ fn init_help() {
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--json Output as JSON [env: FABRO_JSON=]
|
--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=]
|
--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]
|
--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=]
|
--quiet Suppress non-essential output [env: FABRO_QUIET=]
|
||||||
--verbose Enable verbose output [env: FABRO_VERBOSE=]
|
--verbose Enable verbose output [env: FABRO_VERBOSE=]
|
||||||
-h, --help Print help
|
-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]
|
#[test]
|
||||||
fn init_cursor_writes_idempotent_config() {
|
fn init_cursor_writes_idempotent_config() {
|
||||||
let context = test_context!();
|
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]
|
#[test]
|
||||||
fn init_invalid_json_fails_without_overwrite() {
|
fn init_invalid_json_fails_without_overwrite() {
|
||||||
let context = test_context!();
|
let context = test_context!();
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ use anyhow::{Context as _, Result, anyhow};
|
||||||
use serde_json::map::Entry;
|
use serde_json::map::Entry;
|
||||||
use serde_json::{Map, Value, json};
|
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> {
|
pub fn config_json(settings: &McpConfigSettings) -> Result<String> {
|
||||||
serde_json::to_string_pretty(&generic_config(settings))
|
serde_json::to_string_pretty(&generic_config(settings))
|
||||||
|
|
@ -20,17 +20,15 @@ pub fn config_json(settings: &McpConfigSettings) -> Result<String> {
|
||||||
pub fn init_agent(settings: &McpInitSettings) -> Result<()> {
|
pub fn init_agent(settings: &McpInitSettings) -> Result<()> {
|
||||||
let entry = server_entry(&settings.config);
|
let entry = server_entry(&settings.config);
|
||||||
for path in agent_config_paths(settings.agent, &settings.home_dir) {
|
for path in agent_config_paths(settings.agent, &settings.home_dir) {
|
||||||
merge_server_entry(&path, entry.clone())?;
|
merge_server_entry(&path, &settings.config.name, entry.clone())?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generic_config(settings: &McpConfigSettings) -> Value {
|
fn generic_config(settings: &McpConfigSettings) -> Value {
|
||||||
json!({
|
let mut servers = Map::new();
|
||||||
"mcpServers": {
|
servers.insert(settings.name.clone(), server_entry(settings));
|
||||||
SERVER_NAME: server_entry(settings)
|
json!({ "mcpServers": servers })
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn server_entry(settings: &McpConfigSettings) -> Value {
|
fn server_entry(settings: &McpConfigSettings) -> Value {
|
||||||
|
|
@ -53,7 +51,7 @@ fn start_args(settings: &McpConfigSettings) -> Vec<String> {
|
||||||
args
|
args
|
||||||
}
|
}
|
||||||
|
|
||||||
fn merge_server_entry(path: &Path, entry: Value) -> Result<()> {
|
fn merge_server_entry(path: &Path, name: &str, entry: Value) -> Result<()> {
|
||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent() {
|
||||||
std::fs::create_dir_all(parent)
|
std::fs::create_dir_all(parent)
|
||||||
.with_context(|| format!("failed to create {}", parent.display()))?;
|
.with_context(|| format!("failed to create {}", parent.display()))?;
|
||||||
|
|
@ -80,7 +78,7 @@ fn merge_server_entry(path: &Path, entry: Value) -> Result<()> {
|
||||||
path.display()
|
path.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
servers_object.insert(SERVER_NAME.to_string(), entry);
|
servers_object.insert(name.to_string(), entry);
|
||||||
|
|
||||||
let rendered = serde_json::to_string_pretty(&root)
|
let rendered = serde_json::to_string_pretty(&root)
|
||||||
.map(|json| format!("{json}\n"))
|
.map(|json| format!("{json}\n"))
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,9 @@ pub use config::{config_json, init_agent};
|
||||||
use fabro_client::Client;
|
use fabro_client::Client;
|
||||||
pub use server::start;
|
pub use server::start;
|
||||||
|
|
||||||
/// The name this MCP server reports over the wire and registers under in agent
|
/// The name this MCP server reports over the wire. It is also the default
|
||||||
/// config files.
|
/// `mcpServers` key that `fabro mcp config` and `fabro mcp init` register.
|
||||||
pub(crate) const SERVER_NAME: &str = "fabro";
|
pub const SERVER_NAME: &str = "fabro";
|
||||||
|
|
||||||
pub type FabroClientFuture = Pin<Box<dyn Future<Output = Result<Client>> + Send>>;
|
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 {
|
pub struct McpConfigSettings {
|
||||||
|
/// The `mcpServers` key the generated client entry is registered under.
|
||||||
|
pub name: String,
|
||||||
pub server: Option<String>,
|
pub server: Option<String>,
|
||||||
pub storage_dir: Option<PathBuf>,
|
pub storage_dir: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue