From c23120e6c097a6036c17d7ecdf10a40061f3da36 Mon Sep 17 00:00:00 2001 From: Shai Tourchin Date: Tue, 12 May 2026 10:14:38 +0300 Subject: [PATCH 1/3] fix: refuse to start on Node 26+ until qdrant-js gains undici 7 compat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Under Node 26+, the very first qdrant request crashes with `UND_ERR_INVALID_ARG: invalid onError method`. Root cause is a version mismatch: @qdrant/js-client-rest constructs an undici.Agent from its pinned undici ^6 and passes it as the dispatcher to Node's built-in fetch(), which under Node 26 uses a newer undici with stricter dispatcher-hook validation. The bug surfaces on the first real codebase_search / codebase_index call — the MCP handshake succeeds, then everything fails. The error message gives no hint about Node version, so users on Node 26+ lose significant time debugging. This change: - Adds a runtime pre-flight check at index.ts entry that prints a clear actionable error and exits 1. Per ESM the imports below evaluate first, but qdrant-js's module init is side-effect-light, so exiting at the first top-level statement is enough. - Tightens engines.node to `>=18.0.0 <26.0.0` so npm/npx warns at install time. Both can be reverted once one of qdrant/qdrant-js#123 (undici major upgrade) or qdrant/qdrant-js#128 (inject fetch) lands. Refs: qdrant/qdrant-js#134 Co-Authored-By: Claude Opus 4.7 (1M context) --- package-lock.json | 2 +- package.json | 2 +- src/index.ts | 23 +++++++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index b04e4b6..f96825d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,7 +50,7 @@ "vitest": "^4.0.18" }, "engines": { - "node": ">=18.0.0" + "node": ">=18.0.0 <26.0.0" } }, "node_modules/@ast-grep/lang-bash": { diff --git a/package.json b/package.json index 2cf13b1..5d7d91d 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,6 @@ "vitest": "^4.0.18" }, "engines": { - "node": ">=18.0.0" + "node": ">=18.0.0 <26.0.0" } } diff --git a/src/index.ts b/src/index.ts index ebfdb90..8dac571 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,29 @@ // SPDX-License-Identifier: AGPL-3.0-only // Copyright (C) 2026 Giancarlo Erra - Altaire Limited +// Pre-flight: refuse to start on Node versions known to break @qdrant/js-client-rest. +// The qdrant client pins undici ^6 and constructs an undici.Agent it passes to Node's +// built-in fetch() as a dispatcher. Node 26+ ships a stricter undici whose dispatcher +// hook validation rejects the v6 Agent's contract — surfaces as +// `UND_ERR_INVALID_ARG: invalid onError method` on the first qdrant request. +// (The imports below are evaluated before this check at runtime per ESM semantics, +// but qdrant-js's module-init is side-effect-light — only an actual request triggers +// the undici path — so exiting here is enough to spare users the opaque error later.) +// Tracked upstream: https://github.com/qdrant/qdrant-js/issues/134 +// Candidate fixes already in flight: qdrant/qdrant-js#123 (undici major upgrade) and +// qdrant/qdrant-js#128 (inject fetch into REST transport). Once one lands, raise the +// upper bound in package.json's `engines.node` and remove this check. +const nodeMajor = Number.parseInt(process.versions.node.split(".")[0], 10); +if (Number.isFinite(nodeMajor) && nodeMajor >= 26) { + process.stderr.write( + `socraticode: Node ${process.versions.node} is not supported.\n` + + " @qdrant/js-client-rest is incompatible with the undici bundled in Node 26+.\n" + + " Use Node 22.x (via nvm: `nvm install 22 && nvm use 22`, or `brew install node@22` on macOS).\n" + + " See https://github.com/qdrant/qdrant-js/issues/134.\n", + ); + process.exit(1); +} + import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; From 5cd9db07e6c993c8f2fafa756415153afb26da05 Mon Sep 17 00:00:00 2001 From: Shai Tourchin Date: Tue, 12 May 2026 11:06:51 +0300 Subject: [PATCH 2/3] fix(index): flush stderr before exit on Node 26+ guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per CodeRabbit review on #59: process.stderr.write() is async when stderr is piped (every MCP host captures stderr to surface server logs), so a bare `process.exit(1)` immediately after the write terminates synchronously without draining I/O — risking truncation of the compatibility warning that this guard exists to surface. Move the exit into the write callback so the message is guaranteed to flush before the process terminates. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/index.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/index.ts b/src/index.ts index 8dac571..a4dda4a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,13 +16,17 @@ // upper bound in package.json's `engines.node` and remove this check. const nodeMajor = Number.parseInt(process.versions.node.split(".")[0], 10); if (Number.isFinite(nodeMajor) && nodeMajor >= 26) { - process.stderr.write( + // Write-then-exit-in-callback: stderr writes are async when piped (every MCP host + // captures stderr), and a bare `process.exit(1)` terminates the process synchronously + // without draining I/O, risking truncation of this message. + const msg = `socraticode: Node ${process.versions.node} is not supported.\n` + - " @qdrant/js-client-rest is incompatible with the undici bundled in Node 26+.\n" + - " Use Node 22.x (via nvm: `nvm install 22 && nvm use 22`, or `brew install node@22` on macOS).\n" + - " See https://github.com/qdrant/qdrant-js/issues/134.\n", - ); - process.exit(1); + " @qdrant/js-client-rest is incompatible with the undici bundled in Node 26+.\n" + + " Use Node 22.x (via nvm: `nvm install 22 && nvm use 22`, or `brew install node@22` on macOS).\n" + + " See https://github.com/qdrant/qdrant-js/issues/134.\n"; + process.stderr.write(msg, () => { + process.exit(1); + }); } import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; From 69a6b74b8aff6297fd297fb8f995682e72de1053 Mon Sep 17 00:00:00 2001 From: Shai Tourchin Date: Tue, 12 May 2026 11:19:20 +0300 Subject: [PATCH 3/3] fix(index): use fs.writeSync for synchronous flush + sync exit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third-pass review caught that the callback shape from 5cd9db0 (while correctly fixing the stderr-truncation concern CodeRabbit raised) introduced an async-exit window. With process.exit(1) inside the write callback, on Node 26+ the rest of the file's top-level code runs before termination: imports' top-level evaluation, the McpServer/tool registrations, and the start of main()'s connect — the MCP host can briefly see a handshake begin before the process dies. fs.writeSync(2, msg) is the canonical Node pattern for "print fatal error then die" — blocking (no truncation when stderr is piped) AND synchronous (so process.exit(1) runs before any further top-level code). Strictly better than the callback shape on both axes. Also soften comment phrasing to reduce rot risk: - "Candidate fixes already in flight" -> "Upstream PRs under discussion" - "Once one lands" -> "If either lands -- or any other fix supersedes them" Verified: full 4-line stderr message survives piping to a file on Node 26.0.0; exit code 1 preserved. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/index.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/index.ts b/src/index.ts index a4dda4a..953bf85 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,24 +11,25 @@ // but qdrant-js's module-init is side-effect-light — only an actual request triggers // the undici path — so exiting here is enough to spare users the opaque error later.) // Tracked upstream: https://github.com/qdrant/qdrant-js/issues/134 -// Candidate fixes already in flight: qdrant/qdrant-js#123 (undici major upgrade) and -// qdrant/qdrant-js#128 (inject fetch into REST transport). Once one lands, raise the -// upper bound in package.json's `engines.node` and remove this check. +// Upstream PRs under discussion: qdrant/qdrant-js#123 (undici major upgrade) and +// qdrant/qdrant-js#128 (inject fetch into REST transport). If either lands — or any +// other fix supersedes them — raise the upper bound in package.json's `engines.node` +// and remove this check. const nodeMajor = Number.parseInt(process.versions.node.split(".")[0], 10); if (Number.isFinite(nodeMajor) && nodeMajor >= 26) { - // Write-then-exit-in-callback: stderr writes are async when piped (every MCP host - // captures stderr), and a bare `process.exit(1)` terminates the process synchronously - // without draining I/O, risking truncation of this message. + // fs.writeSync(2, …) is the canonical Node idiom for "print fatal error then die": + // blocking (no truncation when stderr is piped — every MCP host pipes stderr) and + // synchronous (so process.exit(1) runs before any further top-level code). const msg = `socraticode: Node ${process.versions.node} is not supported.\n` + " @qdrant/js-client-rest is incompatible with the undici bundled in Node 26+.\n" + " Use Node 22.x (via nvm: `nvm install 22 && nvm use 22`, or `brew install node@22` on macOS).\n" + " See https://github.com/qdrant/qdrant-js/issues/134.\n"; - process.stderr.write(msg, () => { - process.exit(1); - }); + writeSync(2, msg); + process.exit(1); } +import { writeSync } from "node:fs"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod";