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#"