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", + ); +});