From c40a5f043213053cc601befd23017873bceda144 Mon Sep 17 00:00:00 2001 From: "Bob.Hou" Date: Thu, 17 Sep 2026 16:06:41 -0400 Subject: [PATCH] fix(build): externalize @modelcontextprotocol/sdk to heal MCP initialize 500 on standalone builds (#13859) * build/mcp: externalize @modelcontextprotocol/sdk in standalone server bundle The SDK client graph contains a module-level class-extends-Client cycle against the top-level-await Client module. Webpack's TLA runtime evaluates that circular subgraph out of order when it is inlined into route chunks, throwing "Cannot access 'l' before initialization" during module evaluation. Every request to /api/mcp/stream then answers HTTP 500 on initialize and the failed module is evicted and re-evaluated per request, which floods the logs with the same ReferenceError. Node's native ESM loader resolves the same circular graph through live bindings, so keep the SDK external to the server bundle like the other packages that break only when bundled. Signed-off-by: Minxi Hou * changelog: record @modelcontextprotocol/sdk externalize fix (#13859) Signed-off-by: Minxi Hou --------- Signed-off-by: Minxi Hou --- .../fixes/13859-mcp-sdk-externalize.md | 1 + next.config.mjs | 7 ++++++ tests/unit/mcp-sdk-external.test.ts | 25 +++++++++++++++++++ 3 files changed, 33 insertions(+) create mode 100644 changelog.d/fixes/13859-mcp-sdk-externalize.md create mode 100644 tests/unit/mcp-sdk-external.test.ts diff --git a/changelog.d/fixes/13859-mcp-sdk-externalize.md b/changelog.d/fixes/13859-mcp-sdk-externalize.md new file mode 100644 index 0000000000..8a21b7a3e1 --- /dev/null +++ b/changelog.d/fixes/13859-mcp-sdk-externalize.md @@ -0,0 +1 @@ +- fix(build): externalize @modelcontextprotocol/sdk in standalone server webpack config to prevent TDZ ReferenceError on MCP initialize (#13859) diff --git a/next.config.mjs b/next.config.mjs index 1508523280..36c0282e0f 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -366,6 +366,13 @@ const nextConfig = { "ws", "bufferutil", "utf-8-validate", + // The SDK's client graph has a module-level `class extends Client` cycle + // against the TLA Client module. Bundled into route chunks it throws + // "Cannot access 'l' before initialization" during evaluation and every + // /api/mcp/stream initialize answers HTTP 500. Node's native ESM loader + // resolves the same circular graph via live bindings, so keep the SDK + // out of the webpack server bundle. + "@modelcontextprotocol/sdk", "child_process", "fs", "path", diff --git a/tests/unit/mcp-sdk-external.test.ts b/tests/unit/mcp-sdk-external.test.ts new file mode 100644 index 0000000000..2b70bad82d --- /dev/null +++ b/tests/unit/mcp-sdk-external.test.ts @@ -0,0 +1,25 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; + +// The bundled MCP SDK client graph contains a module-level `class extends +// Client` cycle against the async Client module. Under Next's standalone +// webpack TLA runtime that cycle throws `ReferenceError: Cannot access 'l' +// before initialization` during module evaluation, which takes down +// /api/mcp/stream with HTTP 500 on every initialize. Node's native ESM +// loader resolves the same circular graph through live bindings, so the SDK +// must stay external to the server bundle. +test("next.config keeps @modelcontextprotocol/sdk out of the server bundle", () => { + const src = readFileSync(new URL("../../next.config.mjs", import.meta.url), "utf8"); + // Strip // line comments first: a commented-out entry must not satisfy the check. + const uncommented = src + .split("\n") + .map((line) => line.replace(/[/][/].*$/, "")) + .join("\n"); + const externalBlock = uncommented.match(/serverExternalPackages:\s*\[([\s\S]*?)\]/); + assert.ok(externalBlock, "serverExternalPackages block must exist"); + assert.ok( + /["']@modelcontextprotocol[/]sdk["']/.test(externalBlock[1]), + "serverExternalPackages must externalize @modelcontextprotocol/sdk", + ); +});