From 065fbec4b3993ca5903fee1eca691229e330c28a Mon Sep 17 00:00:00 2001 From: gensokyo-dev Date: Sat, 14 Nov 2026 11:37:35 +0000 Subject: [PATCH] fix(session): select the registered entrypoint as the running runtime does Node resolves its script entrypoint lexically, so a registered spelling that crosses a symlink before `..` names the running file lexically, while Bun follows native traversal (#8844). Select the expected server path with the shared runtime-aware entrypoint normalizer before comparing it with the running entrypoint, and fall back to file identity only when they differ. Co-Authored-By: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com> --- src/serverIdentity.ts | 25 ++++++++++++++++++----- tests/boundary/mcp/serverIdentity.test.ts | 21 +++++++++++++++++++ 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/src/serverIdentity.ts b/src/serverIdentity.ts index a6d543843f..d54e9c8913 100644 --- a/src/serverIdentity.ts +++ b/src/serverIdentity.ts @@ -2,6 +2,12 @@ import { statSync } from "node:fs"; import { dirname, basename, resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import { + javaScriptRuntime, + type JavaScriptRuntime, +} from "./domain/runtimeVersion.js"; +import { normalizeEntrypointPath } from "./process/CurrentEntrypointPath.js"; + import type { createCatalogIdentity } from "./catalogIdentity.js"; import { PRODUCT_IDENTITY, SDK_IDENTITY } from "./identity.js"; @@ -28,12 +34,15 @@ const packageRoot = (): string => { const compareServerPaths = ( expected: string, observed: string, + runtime: JavaScriptRuntime["id"], ): "aligned" | "mismatch" | "unknown" => { - if (expected === observed) return "aligned"; - if (process.platform === "win32") - return resolve(expected) === observed ? "aligned" : "mismatch"; + // Select the registered script the way this runtime does: Node resolves it + // lexically, while Bun follows native traversal through a symlink before `..`. + const script = normalizeEntrypointPath(expected, runtime) ?? expected; + if (script === observed) return "aligned"; + if (process.platform === "win32") return "mismatch"; try { - const selected = statSync(expected, { bigint: true }); + const selected = statSync(script, { bigint: true }); const running = statSync(observed, { bigint: true }); return selected.dev === running.dev && selected.ino === running.ino ? "aligned" @@ -51,13 +60,19 @@ export const createServerIdentity = (input: { readonly client?: ConnectedClientIdentity; readonly protocolVersion?: string | undefined; readonly catalog: ReturnType; + /** The runtime that selected `commandPath`; defaults to the current one. */ + readonly runtime?: JavaScriptRuntime["id"]; }) => { const catalog = input.catalog; const serverPath = input.commandPath || undefined; const pathComparison = input.expected?.server_path === undefined || serverPath === undefined ? "unknown" - : compareServerPaths(input.expected.server_path, serverPath); + : compareServerPaths( + input.expected.server_path, + serverPath, + input.runtime ?? javaScriptRuntime(process.versions).id, + ); const reasons: string[] = []; if ( input.expected?.package_version !== undefined && diff --git a/tests/boundary/mcp/serverIdentity.test.ts b/tests/boundary/mcp/serverIdentity.test.ts index 94778aee45..83921ce14f 100644 --- a/tests/boundary/mcp/serverIdentity.test.ts +++ b/tests/boundary/mcp/serverIdentity.test.ts @@ -259,12 +259,14 @@ it.skipIf(process.platform === "win32")( await writeFile(other, "other entrypoint"); await symlink(scripts, join(scratch, "link")); const selected = `${scratch}/link/../scripts/rea.mjs`; + // Bun selects a script through native traversal (#8844). const identity = (server_path: string) => createServerIdentity({ catalog: DEFAULT_CATALOG_IDENTITY, startedAt: "2026-07-13T00:00:00.000Z", commandPath: canonical, expected: { server_path }, + runtime: "bun", }); expect(identity(selected).alignment).toMatchObject({ @@ -279,6 +281,25 @@ it.skipIf(process.platform === "win32")( state: "unknown", reasons: [], }); + // Node resolves its entrypoint lexically, so this spelling runs + // scratch/rea.mjs even though traversal through the link leaves scratch. + // Bun traverses the link and runs a different file. + const lexical = join(scratch, "rea.mjs"); + await writeFile(lexical, "lexical entrypoint"); + await writeFile(join(root, "package", "rea.mjs"), "traversed entrypoint"); + const crossing = (runtime: "node" | "bun") => + createServerIdentity({ + catalog: DEFAULT_CATALOG_IDENTITY, + startedAt: "2026-07-13T00:00:00.000Z", + commandPath: lexical, + expected: { server_path: `${scratch}/link/../rea.mjs` }, + runtime, + }).alignment; + expect(crossing("node")).toMatchObject({ state: "aligned", reasons: [] }); + expect(crossing("bun")).toMatchObject({ + state: "mcp_server_restart_required", + reasons: ["registration_path_mismatch"], + }); }, );