diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 6c5d526e2..5736aabe7 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed macOS stdio MCP servers launching in a detached session, so `xcrun mcpbridge` can trigger the TCC Apple Events permission prompt and complete startup. ([#4987](https://github.com/can1357/oh-my-pi/issues/4987)) + ## [16.3.15] - 2026-07-09 ### Changed diff --git a/packages/coding-agent/src/mcp/transports/stdio.test.ts b/packages/coding-agent/src/mcp/transports/stdio.test.ts index 57a2d161e..ac1364787 100644 --- a/packages/coding-agent/src/mcp/transports/stdio.test.ts +++ b/packages/coding-agent/src/mcp/transports/stdio.test.ts @@ -31,6 +31,18 @@ describe("resolveStdioSpawnCommand", () => { }); }); + it("keeps Darwin stdio MCP servers attached so TCC Apple Events prompts can resolve", async () => { + await expect( + resolveStdioSpawnCommand( + { command: "xcrun", args: ["mcpbridge"] }, + { cwd: process.cwd(), env: {}, platform: "darwin" }, + ), + ).resolves.toEqual({ + cmd: ["xcrun", "mcpbridge"], + detached: false, + }); + }); + it("detaches off-Windows MCP servers so terminal job-control signals cannot stop them", async () => { await expect( resolveStdioSpawnCommand( diff --git a/packages/coding-agent/src/mcp/transports/stdio.ts b/packages/coding-agent/src/mcp/transports/stdio.ts index f9d4e04e0..228226008 100644 --- a/packages/coding-agent/src/mcp/transports/stdio.ts +++ b/packages/coding-agent/src/mcp/transports/stdio.ts @@ -37,13 +37,18 @@ export interface StdioSpawnCommand { */ windowsHide?: boolean; /** - * Run the subprocess in its own session. + * Run the subprocess in its own session when the platform can safely do so. * - * POSIX: `true`. Detach → `setsid`, so the MCP process tree has no - * controlling terminal and terminal job-control signals (Ctrl+Z SIGTSTP, + * Linux/other POSIX: `true`. Detach → `setsid`, so the MCP process tree has + * no controlling terminal and terminal job-control signals (Ctrl+Z SIGTSTP, * background-read SIGTTIN) cannot stop stdio servers such as * `chrome-devtools-mcp` and leave our read loop blocked on silent pipes. * + * macOS: `false`. LaunchServices/TCC attributes Apple Events automation to + * the responsible terminal process only while the child stays in the + * inherited session; detaching via `setsid` prevents the permission prompt + * for servers such as `xcrun mcpbridge` (#4987). + * * Windows: `false`. There is no SIGTSTP/SIGTTIN to escape, and Windows * wrapper chains must stay in the OMP console session so nested console * grandchildren keep stdout routed through our pipe (#3544). @@ -247,7 +252,7 @@ export async function resolveStdioSpawnCommand( options: ResolveStdioSpawnOptions, ): Promise { const args = config.args ?? []; - if (options.platform !== "win32") return { cmd: [config.command, ...args], detached: true }; + if (options.platform !== "win32") return { cmd: [config.command, ...args], detached: options.platform !== "darwin" }; const windowsHide = options.hostHasInheritableConsole === undefined ? true : !options.hostHasInheritableConsole; const resolved = await resolveWindowsCommandPath(config.command, options.cwd, options.env); @@ -366,10 +371,11 @@ export class StdioTransport implements MCPTransport { }); // Platform-derived session and console-window handling come from - // `resolveStdioSpawnCommand`: POSIX detaches into its own session to - // escape terminal job-control signals (SIGTSTP, SIGTTIN); Windows stays - // attached, and only hides the child when the host has no console to - // share. See `StdioSpawnCommand`. + // `resolveStdioSpawnCommand`: Linux/other POSIX detach into their own + // session to escape terminal job-control signals (SIGTSTP, SIGTTIN); + // macOS stays attached so TCC can prompt for Apple Events automation; + // Windows stays attached, and only hides the child when the host has no + // console to share. See `StdioSpawnCommand`. this.#process = spawn({ cmd: spawnCommand.cmd, cwd, diff --git a/packages/coding-agent/src/modes/controllers/input-controller.ts b/packages/coding-agent/src/modes/controllers/input-controller.ts index bb19390b4..f5aa2766a 100644 --- a/packages/coding-agent/src/modes/controllers/input-controller.ts +++ b/packages/coding-agent/src/modes/controllers/input-controller.ts @@ -1024,10 +1024,10 @@ export class InputController { // leaves wrappers and pipeline peers running and the terminal // hung — exactly the failure shape we're fixing. Stopping the whole // group keeps the shell's job-control view consistent. Long-lived - // children that must survive the suspend (MCP stdio servers via - // the `detached: true` spawn in `mcp/transports/stdio.ts`, every - // brush external command via brush's per-child `setsid` in - // `crates/vendor/brush-core/src/commands.rs`) are already in + // children that must survive the suspend (Linux/other POSIX MCP stdio + // servers via the platform-specific `detached: true` spawn in + // `mcp/transports/stdio.ts`, every brush external command via brush's + // per-child `setsid` in `crates/vendor/brush-core/src/commands.rs`) are // their own sessions, so pgid=0 does not reach them. process.kill(0, "SIGSTOP"); } catch (err) {