From 9ecd68905b61c40a80cde0a468f9d0b6d0dfc070 Mon Sep 17 00:00:00 2001 From: David Szarzynski Date: Thu, 16 Jul 2026 17:34:03 -0700 Subject: [PATCH] fix(mcp): execute tool dies on Deno >= 2.9 (unix sockets moved behind net permission) Deno 2.9.0 moved unix sockets behind the net permission as part of the 2.8.2-2.9.1 socket-hardening series (denoland/deno#34395, #35835). deno-http-worker (0.0.21 and 2.0.3 alike) still grants its Node<->sandbox socket via --allow-read/--allow-write, so on Deno >= 2.9 every execute call dies at startup: 'Deno exited before being ready' (NotCapable). Health checks and tools/list still pass, so the breakage is silent. The socket path is generated inside newDenoHTTPWorker and Deno accepts only exact socket paths (no globs) with no repeated --allow-net flags, so no flag can be scoped up front. Use the library's spawnFunc hook to pluck the socket path from the injected --allow-write flag and comma-merge unix: onto the existing --allow-net scope. Gated on Deno >= 2.9 exactly: 2.0-2.8 reject the unix: token at startup ('invalid port') and 1.x panics on it, while their legacy read/write grant keeps working. Verified end-to-end over MCP stdio: published 0.40.0 fails on 2.9.3; this build returns results on both 2.8.3 and 2.9.3; a >=2 gate (the naive fix) provably breaks 2.8.3; non-allowlisted fetch stays denied. Co-Authored-By: Claude Fable 5 --- packages/mcp-server/src/code-tool.ts | 30 ++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/mcp-server/src/code-tool.ts b/packages/mcp-server/src/code-tool.ts index c1ec6259..9eeeb3f5 100644 --- a/packages/mcp-server/src/code-tool.ts +++ b/packages/mcp-server/src/code-tool.ts @@ -159,14 +159,21 @@ const localDenoHandler = async ({ } } - // Deno >= 2 gates unix sockets behind --allow-net=unix:, while Deno 1.x panics on a - // `unix:` token in --allow-net — so the socket grant below must be gated on the major version. - let denoMajor = 2; + // Deno 2.9.0 moved unix sockets behind the net permission (--allow-net=unix:), part of + // the 2.8.2–2.9.1 socket-hardening series (denoland/deno#34395, #35835). Older Denos must NOT + // receive the token: 2.0–2.8 reject it at startup ("invalid port") and 1.x panics — and their + // legacy read/write grant still works. So gate the grant on exactly >= 2.9. + let denoNeedsUnixNetGrant = true; try { const versionOutput = execSync(`"${denoPath}" --version`, { encoding: 'utf8' }); - denoMajor = Number(/deno (\d+)\./.exec(versionOutput)?.[1] ?? denoMajor); + const versionMatch = /deno (\d+)\.(\d+)\./.exec(versionOutput); + if (versionMatch) { + const major = Number(versionMatch[1]); + const minor = Number(versionMatch[2]); + denoNeedsUnixNetGrant = major > 2 || (major === 2 && minor >= 9); + } } catch { - // Version detection failed; assume a current (2.x) Deno. + // Version detection failed; assume a current Deno. } const allowReadPaths = [ @@ -201,11 +208,14 @@ const localDenoHandler = async ({ ], printOutput: true, // deno-http-worker grants its internal unix socket via --allow-read/--allow-write (the - // Deno 1.x permission model), but Deno >= 2 requires --allow-net=unix: or the worker - // dies on startup ("Deno exited before being ready"). The socket path is generated inside - // newDenoHTTPWorker, so pluck it out of the --allow-write flag the library injects and graft - // it onto the --allow-net scope (Deno rejects repeated --allow-net flags, hence the comma). - ...(denoMajor >= 2 && { + // pre-2.9 permission model — unchanged upstream as of 2.0.3, so a version bump is not a fix), + // which on Deno >= 2.9 kills the worker at startup ("Deno exited before being ready"). + // The socket path is generated inside newDenoHTTPWorker, so no flag can be scoped up front + // (Deno accepts only exact socket paths — no globs/prefixes — and rejects repeated + // --allow-net flags). Instead, intercept the spawn, pluck the socket path out of the + // --allow-write flag the library injects, and comma-merge unix: onto the existing + // --allow-net scope. + ...(denoNeedsUnixNetGrant && { spawnFunc: ( command: string, spawnArgs: string[],