From bf6480647491c8fbd7eed960e4506a8c6612b377 Mon Sep 17 00:00:00 2001 From: roboomp Date: Thu, 9 Jul 2026 21:39:42 +0000 Subject: [PATCH] fix(mcp): kept macos stdio servers attached Left Darwin stdio MCP server launches in the inherited session so macOS TCC can prompt for Apple Events permissions used by xcrun mcpbridge. Added resolver coverage for Darwin while preserving Linux detach and Windows console behavior. Fixes #4987 --- packages/coding-agent/CHANGELOG.md | 4 ++++ .../src/mcp/transports/stdio.test.ts | 12 ++++++++++ .../coding-agent/src/mcp/transports/stdio.ts | 22 ++++++++++++------- .../src/modes/controllers/input-controller.ts | 8 +++---- 4 files changed, 34 insertions(+), 12 deletions(-) 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) {