Bake in Xcode's MCP server on macOS
Xcode 26.3+ exposes an MCP server to external agents over stdio (`xcrun mcpbridge`). Now that sigit has a generic stdio transport, add a baked-in `xcode` server on macOS so agents can drive a running Xcode with zero config, mirroring the baked-in official server. It is added only when Xcode is actually running (a cheap `pgrep` check), so there's no startup cost or failed handshake when Xcode is closed. A user-defined `xcode` entry in mcp.toml always wins, and `SIGIT_MCP_XCODE=off` opts out. The server still requires Xcode's "Allow external agents to use Xcode tools" setting to be enabled. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6UuZrohog7z14bLiti7US
Claude committed
Jul 8, 2026 at 12:28 UTC
3275097d0683290466020e27fb11c4ee06fbec44
2 files changed
+83
-2
.agents/AGENTS.md
+5
-2
@@ -134,7 +134,10 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
134
layer and routed in `tools::execute_tool` via `mcp::is_mcp_tool`. The official server
135
(`<cloud>/mcp`, default `https://sigit.si/api/v1/mcp`) is baked in (always HTTP) and authed
136
with the cloud session token; extra servers live in `mcp.toml` (global
137
- `$SIGIT_CONFIG_DIR/mcp.toml` and project-local `.sigit/mcp.toml`). The stdio path is covered by
137
+ `$SIGIT_CONFIG_DIR/mcp.toml` and project-local `.sigit/mcp.toml`). On macOS, Xcode's stdio
138
+ server (`xcrun mcpbridge`) is also baked in — added only when Xcode is running, requiring
139
+ Xcode's "Allow external agents" setting, opt-out via `SIGIT_MCP_XCODE=off`, overridable by a
140
+ user `xcode` entry. The stdio path is covered by
141
`tests/mcp_stdio.rs`, driven by the test-only `src/bin/mcp_stdio_stub.rs` helper binary
142
(excluded from the published crate via `exclude` in `Cargo.toml`).
143
- **`src/permissions.rs`** — tool permission policy. Every tool call passes through
@@ -191,7 +194,7 @@ verbosity with `RUST_LOG`.
194
`OPENAI_BASE_URL` / `OPENAI_API_KEY` (provider override), `SIGIT_API_URL` (account API base,
195
default `https://sigit.si`), `SIGIT_CLOUD_URL`, `SIGIT_CONFIG_DIR` (default `~/.config/sigit`),
196
`SIGIT_MODEL`, `SIGIT_MCP` (`off` disables MCP), `SIGIT_MCP_OFFICIAL` (`off` drops the baked-in
194
-server), `SIGIT_PERMISSIONS` (`allow`/`ask`/`deny` — overrides the default permission mode for
197
+server), `SIGIT_MCP_XCODE` (`off` drops the baked-in macOS Xcode server), `SIGIT_PERMISSIONS` (`allow`/`ask`/`deny` — overrides the default permission mode for
198
mutating tools; the escape hatch for clients without permission-request support),
199
`HF_HOME` / `HF_HUB_CACHE`, `RUST_LOG`.
200
src/mcp.rs
+78
@@ -29,6 +29,17 @@
29
//! credential. Additional servers are configured in `mcp.toml` (see
30
//! [`load_configs`]).
31
//!
32
+//! ## Xcode (macOS)
33
+//!
34
+//! On macOS siGit Code also bakes in [Xcode's MCP server][xcode-mcp] over stdio
35
+//! (`xcrun mcpbridge`), so agents can build, test, and edit a project open in
36
+//! Xcode with no configuration. It is added only when Xcode is actually running,
37
+//! and still requires *Xcode ▸ Settings ▸ Intelligence ▸ "Allow external agents
38
+//! to use Xcode tools"* to be enabled. Opt out with `SIGIT_MCP_XCODE=off`; a
39
+//! user-defined `xcode` entry in `mcp.toml` overrides the baked-in default.
40
+//!
41
+//! [xcode-mcp]: https://developer.apple.com/documentation/xcode/giving-external-agents-access-to-xcode
42
+//!
43
//! ## Lifecycle
44
//!
45
//! Discovery is best-effort and happens once at startup via [`init`]: each
@@ -433,9 +444,53 @@ fn load_configs() -> Vec<ServerDef> {
444
});
445
}
446
447
+ // On macOS, bake in Xcode's stdio MCP server (`xcrun mcpbridge`) so agents
448
+ // can drive a running Xcode with zero config. It is added only when Xcode is
449
+ // actually running — spawning `mcpbridge` with no Xcode to bridge into is
450
+ // pointless and would only add a failed handshake to every startup. A
451
+ // user-defined entry named `xcode` (e.g. a custom command) always wins, and
452
+ // `SIGIT_MCP_XCODE=off` opts out entirely. Note the server still requires
453
+ // Xcode ▸ Settings ▸ Intelligence ▸ "Allow external agents to use Xcode
454
+ // tools" to be enabled; without it the handshake fails and it contributes
455
+ // no tools.
456
+ #[cfg(target_os = "macos")]
457
+ if !xcode_opted_out() && is_xcode_running() && !defs.iter().any(|d| d.name == "xcode") {
458
+ defs.push(ServerDef {
459
+ name: "xcode".to_string(),
460
+ transport: TransportDef::Stdio {
461
+ command: "xcrun".to_string(),
462
+ args: vec!["mcpbridge".to_string()],
463
+ env: Vec::new(),
464
+ },
465
+ });
466
+ }
467
+
468
defs
469
}
470
471
+/// Whether the baked-in Xcode server is disabled via `SIGIT_MCP_XCODE=off`.
472
+#[cfg(target_os = "macos")]
473
+fn xcode_opted_out() -> bool {
474
+ std::env::var("SIGIT_MCP_XCODE").is_ok_and(|value| {
475
+ matches!(
476
+ value.trim().to_ascii_lowercase().as_str(),
477
+ "off" | "0" | "false" | "no" | "disabled"
478
+ )
479
+ })
480
+}
481
+
482
+/// Best-effort check for a running Xcode, so the baked-in `xcode` server is only
483
+/// added when there's something for `xcrun mcpbridge` to bridge into. Any error
484
+/// (e.g. `pgrep` missing) is treated as "not running".
485
+#[cfg(target_os = "macos")]
486
+fn is_xcode_running() -> bool {
487
+ std::process::Command::new("pgrep")
488
+ .args(["-xq", "Xcode"])
489
+ .status()
490
+ .map(|status| status.success())
491
+ .unwrap_or(false)
492
+}
493
+
494
/// Insert `def`, replacing any existing entry with the same name.
495
fn upsert(defs: &mut Vec<ServerDef>, def: ServerDef) {
496
if let Some(slot) = defs.iter_mut().find(|d| d.name == def.name) {
@@ -1339,6 +1394,29 @@ mod tests {
1394
}
1395
}
1396
1397
+ #[test]
1398
+ fn user_xcode_entry_resolves_to_its_stdio_command() {
1399
+ // A user-defined `xcode` server in mcp.toml overrides the baked-in macOS
1400
+ // default (which uses `xcrun mcpbridge`); it must resolve like any other
1401
+ // stdio entry.
1402
+ let toml = r#"
1403
+ [[server]]
1404
+ name = "xcode"
1405
+ command = "xcrun"
1406
+ args = ["mcpbridge"]
1407
+ "#;
1408
+ let parsed: McpFile = toml::from_str(toml).unwrap();
1409
+ let entry = &parsed.server[0];
1410
+ assert_eq!(sanitize(&entry.name), "xcode");
1411
+ match entry.transport_def().expect("valid stdio entry") {
1412
+ TransportDef::Stdio { command, args, .. } => {
1413
+ assert_eq!(command, "xcrun");
1414
+ assert_eq!(args, vec!["mcpbridge".to_string()]);
1415
+ }
1416
+ TransportDef::Http { .. } => panic!("expected a stdio transport"),
1417
+ }
1418
+ }
1419
+
1420
#[test]
1421
fn entry_with_url_and_command_is_a_config_error() {
1422
let toml = r#"