feat: bake in the smbCloud CLI's stdio MCP server
sigit already bakes in its official HTTP MCP server. This adds a second built-in entry: the smbCloud CLI's stdio server (smb --mcp), so smbCloud project and deployment tools are available without any mcp.toml setup. The entry is only added when the smb binary is actually on PATH, so users without smbcloud-cli installed see nothing rather than a spawn failure in /mcp. A user-defined server named smbcloud overrides the baked-in one, and it can be switched off with smbcloud = false in mcp.toml or SIGIT_MCP_SMBCLOUD=off, mirroring the official server's opt-outs.
paydii committed
Jul 19, 2026 at 13:55 UTC
7742455ea7656a17b6543c02983cf6edb9b34d9a
2 files changed
+91
-7
.agents/AGENTS.md
+8
-4
@@ -172,9 +172,12 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
172
both read it; `/reload` does *not* re-run it, so config changes need a restart. stdio children
173
live for the process; a dead child fails calls with an in-band error string (no auto-restart).
174
Tools are namespaced `mcp__<server>__<tool>`, appended in the `*_as_specs`/`build_tool_specs`
175
- layer and routed in `tools::execute_tool` via `mcp::is_mcp_tool`. The official server
176
- (`<cloud>/mcp`, default `https://sigit.si/api/v1/mcp`) is baked in (always HTTP) and authed
177
- with the cloud session token; extra servers live in `mcp.toml` (global
175
+ layer and routed in `tools::execute_tool` via `mcp::is_mcp_tool`. Two servers are baked in:
176
+ the official server (`<cloud>/mcp`, default `https://sigit.si/api/v1/mcp`, always HTTP, authed
177
+ with the cloud session token) and the smbCloud CLI server (`smb --mcp`, stdio, added only when
178
+ the `smb` binary is on `PATH`; opt out with `smbcloud = false` in `mcp.toml` or
179
+ `SIGIT_MCP_SMBCLOUD=off`). A user-defined entry named `sigit` or `smbcloud` overrides the
180
+ corresponding baked-in one. Extra servers live in `mcp.toml` (global
181
`$SIGIT_CONFIG_DIR/mcp.toml` and project-local `.sigit/mcp.toml`). The stdio path is covered by
182
`tests/mcp_stdio.rs`, driven by the test-only `src/bin/mcp_stdio_stub.rs` helper binary
183
(excluded from the published crate via `exclude` in `Cargo.toml`).
@@ -233,7 +236,8 @@ verbosity with `RUST_LOG`.
236
237
`OPENAI_BASE_URL` / `OPENAI_API_KEY` (provider override), `SIGIT_API_URL` (account API base,
238
default `https://sigit.si`), `SIGIT_CLOUD_URL`, `SIGIT_CONFIG_DIR` (default `~/.config/sigit`),
236
-`SIGIT_MODEL`, `SIGIT_MCP` (`off` disables MCP), `SIGIT_MCP_OFFICIAL` (`off` drops the baked-in
239
+`SIGIT_MODEL`, `SIGIT_MCP` (`off` disables MCP), `SIGIT_MCP_SMBCLOUD` (`off` drops the baked-in
240
+smbCloud CLI server), `SIGIT_MCP_OFFICIAL` (`off` drops the baked-in
241
server), `SIGIT_PERMISSIONS` (`allow`/`ask`/`deny` — overrides the default permission mode for
242
mutating tools; the escape hatch for clients without permission-request support),
243
`HF_HOME` / `HF_HUB_CACHE`, `RUST_LOG`.
src/mcp.rs
+83
-3
@@ -21,13 +21,21 @@
21
//! `url` and `command` are mutually exclusive; an entry with both, or neither,
22
//! is a config error that is logged and skipped.
23
//!
24
-//! ## The official server
24
+//! ## Baked-in servers
25
//!
26
//! siGit Code bakes in its official MCP server at `<cloud>/mcp` (default
27
//! `https://sigit.si/api/v1/mcp`, following `SIGIT_CLOUD_URL`). When the user is
28
//! signed in (`sigit login`) the cloud session token is sent as a bearer
29
-//! credential. Additional servers are configured in `mcp.toml` (see
30
-//! [`load_configs`]).
29
+//! credential.
30
+//!
31
+//! The smbCloud CLI's MCP server (`smb --mcp`, stdio) is also baked in, but
32
+//! only when the `smb` binary is actually on `PATH` — no binary, no entry, no
33
+//! error. Opt out with `smbcloud = false` in `mcp.toml` or
34
+//! `SIGIT_MCP_SMBCLOUD=off`.
35
+//!
36
+//! Both baked-in entries yield to a user-defined `mcp.toml` server of the same
37
+//! name, so either can be repointed or reconfigured without a special case.
38
+//! Additional servers are configured in `mcp.toml` (see [`load_configs`]).
39
//!
40
//! ## Lifecycle
41
//!
@@ -94,6 +102,14 @@ pub fn official_tool_suffix(name: &str) -> Option<&str> {
102
.strip_prefix("__")
103
}
104
105
+/// Name of the baked-in smbCloud CLI server; its tools are namespaced
106
+/// `mcp__smbcloud__<tool>`. Like the official server, a user-defined `mcp.toml`
107
+/// entry with this name overrides the baked-in command line.
108
+const SMBCLOUD_SERVER_NAME: &str = "smbcloud";
109
+
110
+/// The smbCloud CLI binary the baked-in stdio entry spawns (`smb --mcp`).
111
+const SMBCLOUD_COMMAND: &str = "smb";
112
+
113
/// JSON-RPC / MCP protocol version we advertise in the handshake.
114
const PROTOCOL_VERSION: &str = "2025-06-18";
115
@@ -303,6 +319,10 @@ struct McpFile {
319
/// opt out.
320
#[serde(default)]
321
official: Option<bool>,
322
+ /// Include the baked-in smbCloud CLI server (`smb --mcp`). Defaults to
323
+ /// `true`; set `false` to opt out. Moot when `smb` isn't installed.
324
+ #[serde(default)]
325
+ smbcloud: Option<bool>,
326
#[serde(default)]
327
server: Vec<ServerEntry>,
328
}
@@ -386,6 +406,7 @@ fn load_configs() -> Vec<ServerDef> {
406
}
407
408
let mut include_official = true;
409
+ let mut include_smbcloud = true;
410
// De-duplicated by sanitized name; a later config file overrides an earlier
411
// one for the same name (project-local wins over global).
412
let mut defs: Vec<ServerDef> = Vec::new();
@@ -404,6 +425,9 @@ fn load_configs() -> Vec<ServerDef> {
425
if let Some(official) = parsed.official {
426
include_official = official;
427
}
428
+ if let Some(smbcloud) = parsed.smbcloud {
429
+ include_smbcloud = smbcloud;
430
+ }
431
for entry in parsed.server {
432
if entry.enabled == Some(false) {
433
continue;
@@ -453,9 +477,48 @@ fn load_configs() -> Vec<ServerDef> {
477
});
478
}
479
480
+ // The smbCloud server can also be disabled with SIGIT_MCP_SMBCLOUD=off.
481
+ if let Ok(value) = std::env::var("SIGIT_MCP_SMBCLOUD")
482
+ && matches!(
483
+ value.trim().to_ascii_lowercase().as_str(),
484
+ "off" | "0" | "false" | "no"
485
+ )
486
+ {
487
+ include_smbcloud = false;
488
+ }
489
+
490
+ // Add the baked-in smbCloud CLI server, with the same never-clobber rule as
491
+ // the official one. Only when `smb` is actually installed: a hardwired
492
+ // entry for a binary most users don't have would surface a spawn failure
493
+ // in `/mcp` instead of just staying out of the way.
494
+ if include_smbcloud && !defs.iter().any(|d| d.name == SMBCLOUD_SERVER_NAME) {
495
+ if on_path(SMBCLOUD_COMMAND) {
496
+ defs.push(ServerDef {
497
+ name: SMBCLOUD_SERVER_NAME.to_string(),
498
+ transport: TransportDef::Stdio {
499
+ command: SMBCLOUD_COMMAND.to_string(),
500
+ args: vec!["--mcp".to_string()],
501
+ env: Vec::new(),
502
+ },
503
+ });
504
+ } else {
505
+ log::debug!("mcp: `{SMBCLOUD_COMMAND}` not on PATH; skipping the smbcloud server");
506
+ }
507
+ }
508
+
509
defs
510
}
511
512
+/// Whether `binary` resolves to an executable file on `PATH` (with the
513
+/// platform's executable suffix, `.exe` on Windows).
514
+fn on_path(binary: &str) -> bool {
515
+ let Some(path) = std::env::var_os("PATH") else {
516
+ return false;
517
+ };
518
+ let file = format!("{binary}{}", std::env::consts::EXE_SUFFIX);
519
+ std::env::split_paths(&path).any(|dir| !dir.as_os_str().is_empty() && dir.join(&file).is_file())
520
+}
521
+
522
/// Insert `def`, replacing any existing entry with the same name.
523
fn upsert(defs: &mut Vec<ServerDef>, def: ServerDef) {
524
if let Some(slot) = defs.iter_mut().find(|d| d.name == def.name) {
@@ -1335,6 +1398,7 @@ mod tests {
1398
"#;
1399
let parsed: McpFile = toml::from_str(toml).unwrap();
1400
assert_eq!(parsed.official, Some(false));
1401
+ assert_eq!(parsed.smbcloud, None);
1402
assert_eq!(parsed.server.len(), 2);
1403
assert_eq!(parsed.server[0].name, "github");
1404
assert_eq!(parsed.server[1].enabled, Some(false));
@@ -1347,6 +1411,22 @@ mod tests {
1411
);
1412
}
1413
1414
+ #[test]
1415
+ fn parses_smbcloud_opt_out_flag() {
1416
+ let parsed: McpFile = toml::from_str("smbcloud = false").unwrap();
1417
+ assert_eq!(parsed.smbcloud, Some(false));
1418
+ // Absent means "include" (the default stays true in load_configs).
1419
+ let parsed: McpFile = toml::from_str("").unwrap();
1420
+ assert_eq!(parsed.smbcloud, None);
1421
+ }
1422
+
1423
+ #[test]
1424
+ fn on_path_finds_real_binaries_only() {
1425
+ assert!(!on_path("definitely-not-a-real-binary-xyzzy"));
1426
+ #[cfg(unix)]
1427
+ assert!(on_path("sh"));
1428
+ }
1429
+
1430
#[test]
1431
fn parses_stdio_server_with_args_and_env() {
1432
let toml = r#"