From 9634539b499850e0231fc6da82a6d4e0595b6835 Mon Sep 17 00:00:00 2001 From: David Szarzynski Date: Thu, 16 Jul 2026 17:18:20 -0700 Subject: [PATCH] fix(mcp): execute tool dies on Deno >= 2 (unix socket needs net permission) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deno 2.x moved unix sockets under the net permission: listening on the socket that deno-http-worker uses to talk to the sandbox now requires --allow-net=unix:. The library (0.0.21, and still 2.0.3) only grants the socket via --allow-read/--allow-write — the Deno 1.x model — so on any modern Deno every execute call fails with 'Deno exited before being ready' (NotCapable: Requires net access to unix:...). Health checks and tools/list still pass, so the breakage is silent. The socket path is generated inside newDenoHTTPWorker, so we can't scope a flag up front (Deno only accepts exact socket paths, no globs/prefixes, and rejects repeated --allow-net flags). Instead, use the library's spawnFunc hook to pluck the socket path out of the --allow-write flag it injects and graft unix: onto the existing --allow-net scope at spawn time. Gated on Deno major >= 2 because Deno 1.46 panics outright on a unix: token in --allow-net. Verified end-to-end over MCP stdio against Deno 2.9.3: published 0.40.0 fails, this build returns results; fetch to a non-allowlisted host is still denied (sandbox scoping unchanged). Co-Authored-By: Claude Fable 5 --- packages/mcp-server/src/code-tool.ts | 35 +++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/packages/mcp-server/src/code-tool.ts b/packages/mcp-server/src/code-tool.ts index edab6620..c1ec6259 100644 --- a/packages/mcp-server/src/code-tool.ts +++ b/packages/mcp-server/src/code-tool.ts @@ -141,7 +141,7 @@ const localDenoHandler = async ({ const packageNodeModulesPath = path.resolve(packageRoot, 'node_modules'); // Check if deno is in PATH - const { execSync } = await import('node:child_process'); + const { execSync, spawn } = await import('node:child_process'); try { execSync('command -v deno', { stdio: 'ignore' }); denoPath = 'deno'; @@ -159,6 +159,16 @@ 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; + try { + const versionOutput = execSync(`"${denoPath}" --version`, { encoding: 'utf8' }); + denoMajor = Number(/deno (\d+)\./.exec(versionOutput)?.[1] ?? denoMajor); + } catch { + // Version detection failed; assume a current (2.x) Deno. + } + const allowReadPaths = [ 'code-tool-worker.mjs', `${workerPath.replace(/([\/\\]node_modules)[\/\\].+$/, '$1')}/`, @@ -190,6 +200,29 @@ const localDenoHandler = async ({ '--allow-env', ], 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 && { + spawnFunc: ( + command: string, + spawnArgs: string[], + options: import('node:child_process').SpawnOptions, + ) => { + const socketPath = spawnArgs + .find((flag) => flag.startsWith('--allow-write=')) + ?.slice('--allow-write='.length) + .split(',') + .find((segment) => segment.endsWith('-deno-http.sock')); + const patchedArgs = + socketPath ? + spawnArgs.map((flag) => (flag.startsWith('--allow-net=') ? `${flag},unix:${socketPath}` : flag)) + : spawnArgs; + return spawn(command, patchedArgs, options); + }, + }), spawnOptions: { cwd: path.dirname(workerPath), // Merge any upstream client envs into the Deno subprocess environment,