From 6e48b526af88f278604fd7ac376084da100505cb Mon Sep 17 00:00:00 2001 From: Ahmed Saad Date: Sun, 2 Aug 2026 14:01:17 +0300 Subject: [PATCH 1/6] fix(everything): update future mcp versions dynamically from package.json Athough this one was is in sync with package.json, it should get updated manually in the future. --- src/everything/server/index.ts | 3 ++- src/everything/version.ts | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 src/everything/version.ts diff --git a/src/everything/server/index.ts b/src/everything/server/index.ts index f1459cc812..a03186f5a3 100644 --- a/src/everything/server/index.ts +++ b/src/everything/server/index.ts @@ -12,6 +12,7 @@ import { registerResources, readInstructions } from "../resources/index.js"; import { registerPrompts } from "../prompts/index.js"; import { stopSimulatedLogging } from "./logging.js"; import { syncRoots } from "./roots.js"; +import { resolvePackageVersion } from "../version.js"; // Server Factory response export type ServerFactoryResponse = { @@ -47,7 +48,7 @@ export const createServer: () => ServerFactoryResponse = () => { { name: "mcp-servers/everything", title: "Everything Reference Server", - version: "2.0.0", + version: resolvePackageVersion(), }, { capabilities: { diff --git a/src/everything/version.ts b/src/everything/version.ts new file mode 100644 index 0000000000..5dde4b3a94 --- /dev/null +++ b/src/everything/version.ts @@ -0,0 +1,33 @@ +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +/** Only "manifest isn't here" is skippable; other errors propagate. */ +const isMissingFile = (error: unknown): boolean => { + const code = (error as NodeJS.ErrnoException)?.code; + return code === "ENOENT" || code === "ENOTDIR"; +}; + +/** + * Reads this package's version from package.json. release.py stamps the version + * at release time, so a literal in source is always stale. + */ +export const resolvePackageVersion = (fallback = "0.0.0-dev"): string => { + const moduleDir = dirname(fileURLToPath(import.meta.url)); + + // Manifest sits alongside this module from source, one level up from dist/. + for (const dir of [moduleDir, dirname(moduleDir)]) { + let manifest: string; + try { + manifest = readFileSync(join(dir, "package.json"), "utf8"); + } catch (error) { + if (isMissingFile(error)) continue; + throw error; + } + + const { version } = JSON.parse(manifest); + if (typeof version === "string") return version; + } + + return fallback; +}; From b49b76d56acfd0438073f73a1fe90e8ac3b55179 Mon Sep 17 00:00:00 2001 From: Ahmed Saad Date: Sun, 2 Aug 2026 14:05:20 +0300 Subject: [PATCH 2/6] fix(filesystem): apply dynamic server version reading --- src/filesystem/index.ts | 3 ++- src/filesystem/version.ts | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 src/filesystem/version.ts diff --git a/src/filesystem/index.ts b/src/filesystem/index.ts index 234605bb13..47f6e9d488 100644 --- a/src/filesystem/index.ts +++ b/src/filesystem/index.ts @@ -27,6 +27,7 @@ import { headFile, setAllowedDirectories, } from './lib.js'; +import { resolvePackageVersion } from './version.js'; // Command line argument parsing const args = process.argv.slice(2); @@ -163,7 +164,7 @@ const GetFileInfoArgsSchema = z.object({ const server = new McpServer( { name: "secure-filesystem-server", - version: "0.2.0", + version: resolvePackageVersion(), } ); diff --git a/src/filesystem/version.ts b/src/filesystem/version.ts new file mode 100644 index 0000000000..b4aeb2b68a --- /dev/null +++ b/src/filesystem/version.ts @@ -0,0 +1,33 @@ +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +/** Only "manifest isn't here" is skippable; other errors propagate. */ +const isMissingFile = (error: unknown): boolean => { + const code = (error as NodeJS.ErrnoException)?.code; + return code === 'ENOENT' || code === 'ENOTDIR'; +}; + +/** + * Reads this package's version from package.json. release.py stamps the version + * at release time, so a literal in source is always stale. + */ +export const resolvePackageVersion = (fallback = '0.0.0-dev'): string => { + const moduleDir = dirname(fileURLToPath(import.meta.url)); + + // Manifest sits alongside this module from source, one level up from dist/. + for (const dir of [moduleDir, dirname(moduleDir)]) { + let manifest: string; + try { + manifest = readFileSync(join(dir, 'package.json'), 'utf8'); + } catch (error) { + if (isMissingFile(error)) continue; + throw error; + } + + const { version } = JSON.parse(manifest); + if (typeof version === 'string') return version; + } + + return fallback; +}; From 043f2fad9d2113389b270119a484b3d375428529 Mon Sep 17 00:00:00 2001 From: Ahmed Saad Date: Sun, 2 Aug 2026 14:11:22 +0300 Subject: [PATCH 3/6] fix(memory): apply dynamic server version ready to server --- src/memory/index.ts | 3 ++- src/memory/version.ts | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 src/memory/version.ts diff --git a/src/memory/index.ts b/src/memory/index.ts index 9865c5318e..b2ce217833 100644 --- a/src/memory/index.ts +++ b/src/memory/index.ts @@ -7,6 +7,7 @@ import { z } from "zod"; import { promises as fs } from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; +import { resolvePackageVersion } from './version.js'; // Define memory file path using environment variable with fallback export const defaultMemoryPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'memory.jsonl'); @@ -256,7 +257,7 @@ const RelationSchema = z.object({ // The server instance and tools exposed to Claude const server = new McpServer({ name: "memory-server", - version: "0.6.3", + version: resolvePackageVersion(), }); const RESOURCE_URI = "memory://knowledge-graph"; diff --git a/src/memory/version.ts b/src/memory/version.ts new file mode 100644 index 0000000000..b4aeb2b68a --- /dev/null +++ b/src/memory/version.ts @@ -0,0 +1,33 @@ +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +/** Only "manifest isn't here" is skippable; other errors propagate. */ +const isMissingFile = (error: unknown): boolean => { + const code = (error as NodeJS.ErrnoException)?.code; + return code === 'ENOENT' || code === 'ENOTDIR'; +}; + +/** + * Reads this package's version from package.json. release.py stamps the version + * at release time, so a literal in source is always stale. + */ +export const resolvePackageVersion = (fallback = '0.0.0-dev'): string => { + const moduleDir = dirname(fileURLToPath(import.meta.url)); + + // Manifest sits alongside this module from source, one level up from dist/. + for (const dir of [moduleDir, dirname(moduleDir)]) { + let manifest: string; + try { + manifest = readFileSync(join(dir, 'package.json'), 'utf8'); + } catch (error) { + if (isMissingFile(error)) continue; + throw error; + } + + const { version } = JSON.parse(manifest); + if (typeof version === 'string') return version; + } + + return fallback; +}; From 471f2001378670c175f243c877ed39d2376ae726 Mon Sep 17 00:00:00 2001 From: Ahmed Saad Date: Sun, 2 Aug 2026 14:53:03 +0300 Subject: [PATCH 4/6] test: cover dynamic server version resolution --- src/everything/__tests__/version.test.ts | 103 +++++++++++++++++++++++ src/filesystem/__tests__/version.test.ts | 103 +++++++++++++++++++++++ src/memory/__tests__/version.test.ts | 103 +++++++++++++++++++++++ 3 files changed, 309 insertions(+) create mode 100644 src/everything/__tests__/version.test.ts create mode 100644 src/filesystem/__tests__/version.test.ts create mode 100644 src/memory/__tests__/version.test.ts diff --git a/src/everything/__tests__/version.test.ts b/src/everything/__tests__/version.test.ts new file mode 100644 index 0000000000..62bb97b659 --- /dev/null +++ b/src/everything/__tests__/version.test.ts @@ -0,0 +1,103 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { resolvePackageVersion } from '../version.js'; + +vi.mock('node:fs', async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, readFileSync: vi.fn(actual.readFileSync) }; +}); + +const actualFs = await vi.importActual('node:fs'); +const readFileSyncMock = vi.mocked(readFileSync); + +const packageRoot = dirname(dirname(fileURLToPath(import.meta.url))); +const { version } = JSON.parse(actualFs.readFileSync(join(packageRoot, 'package.json'), 'utf8')); + +/** fs errors carry a `code`; only ENOENT/ENOTDIR mean "not at this path". */ +const fsError = (code: string) => Object.assign(new Error(code), { code }); + +beforeEach(() => { + readFileSyncMock.mockReset(); + readFileSyncMock.mockImplementation(actualFs.readFileSync); +}); + +describe('resolvePackageVersion', () => { + it('reports the version from package.json', () => { + expect(resolvePackageVersion()).toBe(version); + }); + + it('does not fall back while the manifest is readable', () => { + expect(resolvePackageVersion('unused-fallback')).toBe(version); + }); + + it('falls back when no manifest is found', () => { + readFileSyncMock.mockImplementation(() => { + throw fsError('ENOENT'); + }); + + expect(resolvePackageVersion('1.2.3-fallback')).toBe('1.2.3-fallback'); + }); + + it('falls back to 0.0.0-dev by default', () => { + readFileSyncMock.mockImplementation(() => { + throw fsError('ENOENT'); + }); + + expect(resolvePackageVersion()).toBe('0.0.0-dev'); + }); + + it('checks the parent directory, covering the dist/ layout', () => { + readFileSyncMock + .mockImplementationOnce(() => { + throw fsError('ENOENT'); + }) + .mockImplementationOnce(() => JSON.stringify({ version: '9.9.9' })); + + expect(resolvePackageVersion()).toBe('9.9.9'); + }); + + it('treats ENOTDIR as a missing manifest', () => { + readFileSyncMock + .mockImplementationOnce(() => { + throw fsError('ENOTDIR'); + }) + .mockImplementationOnce(() => JSON.stringify({ version: '8.8.8' })); + + expect(resolvePackageVersion()).toBe('8.8.8'); + }); + + it('skips a manifest that has no version field', () => { + readFileSyncMock + .mockImplementationOnce(() => JSON.stringify({ name: 'no-version-here' })) + .mockImplementationOnce(() => JSON.stringify({ version: '7.7.7' })); + + expect(resolvePackageVersion()).toBe('7.7.7'); + }); + + it('stops searching at the package root', () => { + readFileSyncMock.mockImplementation(() => { + throw fsError('ENOENT'); + }); + + resolvePackageVersion(); + + expect(readFileSyncMock).toHaveBeenCalledTimes(2); + }); + + it('propagates errors other than a missing manifest', () => { + const denied = fsError('EACCES'); + readFileSyncMock.mockImplementation(() => { + throw denied; + }); + + expect(() => resolvePackageVersion()).toThrow(denied); + }); + + it('propagates a malformed manifest', () => { + readFileSyncMock.mockImplementation(() => '{ "version": '); + + expect(() => resolvePackageVersion()).toThrow(SyntaxError); + }); +}); diff --git a/src/filesystem/__tests__/version.test.ts b/src/filesystem/__tests__/version.test.ts new file mode 100644 index 0000000000..62bb97b659 --- /dev/null +++ b/src/filesystem/__tests__/version.test.ts @@ -0,0 +1,103 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { resolvePackageVersion } from '../version.js'; + +vi.mock('node:fs', async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, readFileSync: vi.fn(actual.readFileSync) }; +}); + +const actualFs = await vi.importActual('node:fs'); +const readFileSyncMock = vi.mocked(readFileSync); + +const packageRoot = dirname(dirname(fileURLToPath(import.meta.url))); +const { version } = JSON.parse(actualFs.readFileSync(join(packageRoot, 'package.json'), 'utf8')); + +/** fs errors carry a `code`; only ENOENT/ENOTDIR mean "not at this path". */ +const fsError = (code: string) => Object.assign(new Error(code), { code }); + +beforeEach(() => { + readFileSyncMock.mockReset(); + readFileSyncMock.mockImplementation(actualFs.readFileSync); +}); + +describe('resolvePackageVersion', () => { + it('reports the version from package.json', () => { + expect(resolvePackageVersion()).toBe(version); + }); + + it('does not fall back while the manifest is readable', () => { + expect(resolvePackageVersion('unused-fallback')).toBe(version); + }); + + it('falls back when no manifest is found', () => { + readFileSyncMock.mockImplementation(() => { + throw fsError('ENOENT'); + }); + + expect(resolvePackageVersion('1.2.3-fallback')).toBe('1.2.3-fallback'); + }); + + it('falls back to 0.0.0-dev by default', () => { + readFileSyncMock.mockImplementation(() => { + throw fsError('ENOENT'); + }); + + expect(resolvePackageVersion()).toBe('0.0.0-dev'); + }); + + it('checks the parent directory, covering the dist/ layout', () => { + readFileSyncMock + .mockImplementationOnce(() => { + throw fsError('ENOENT'); + }) + .mockImplementationOnce(() => JSON.stringify({ version: '9.9.9' })); + + expect(resolvePackageVersion()).toBe('9.9.9'); + }); + + it('treats ENOTDIR as a missing manifest', () => { + readFileSyncMock + .mockImplementationOnce(() => { + throw fsError('ENOTDIR'); + }) + .mockImplementationOnce(() => JSON.stringify({ version: '8.8.8' })); + + expect(resolvePackageVersion()).toBe('8.8.8'); + }); + + it('skips a manifest that has no version field', () => { + readFileSyncMock + .mockImplementationOnce(() => JSON.stringify({ name: 'no-version-here' })) + .mockImplementationOnce(() => JSON.stringify({ version: '7.7.7' })); + + expect(resolvePackageVersion()).toBe('7.7.7'); + }); + + it('stops searching at the package root', () => { + readFileSyncMock.mockImplementation(() => { + throw fsError('ENOENT'); + }); + + resolvePackageVersion(); + + expect(readFileSyncMock).toHaveBeenCalledTimes(2); + }); + + it('propagates errors other than a missing manifest', () => { + const denied = fsError('EACCES'); + readFileSyncMock.mockImplementation(() => { + throw denied; + }); + + expect(() => resolvePackageVersion()).toThrow(denied); + }); + + it('propagates a malformed manifest', () => { + readFileSyncMock.mockImplementation(() => '{ "version": '); + + expect(() => resolvePackageVersion()).toThrow(SyntaxError); + }); +}); diff --git a/src/memory/__tests__/version.test.ts b/src/memory/__tests__/version.test.ts new file mode 100644 index 0000000000..62bb97b659 --- /dev/null +++ b/src/memory/__tests__/version.test.ts @@ -0,0 +1,103 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { resolvePackageVersion } from '../version.js'; + +vi.mock('node:fs', async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, readFileSync: vi.fn(actual.readFileSync) }; +}); + +const actualFs = await vi.importActual('node:fs'); +const readFileSyncMock = vi.mocked(readFileSync); + +const packageRoot = dirname(dirname(fileURLToPath(import.meta.url))); +const { version } = JSON.parse(actualFs.readFileSync(join(packageRoot, 'package.json'), 'utf8')); + +/** fs errors carry a `code`; only ENOENT/ENOTDIR mean "not at this path". */ +const fsError = (code: string) => Object.assign(new Error(code), { code }); + +beforeEach(() => { + readFileSyncMock.mockReset(); + readFileSyncMock.mockImplementation(actualFs.readFileSync); +}); + +describe('resolvePackageVersion', () => { + it('reports the version from package.json', () => { + expect(resolvePackageVersion()).toBe(version); + }); + + it('does not fall back while the manifest is readable', () => { + expect(resolvePackageVersion('unused-fallback')).toBe(version); + }); + + it('falls back when no manifest is found', () => { + readFileSyncMock.mockImplementation(() => { + throw fsError('ENOENT'); + }); + + expect(resolvePackageVersion('1.2.3-fallback')).toBe('1.2.3-fallback'); + }); + + it('falls back to 0.0.0-dev by default', () => { + readFileSyncMock.mockImplementation(() => { + throw fsError('ENOENT'); + }); + + expect(resolvePackageVersion()).toBe('0.0.0-dev'); + }); + + it('checks the parent directory, covering the dist/ layout', () => { + readFileSyncMock + .mockImplementationOnce(() => { + throw fsError('ENOENT'); + }) + .mockImplementationOnce(() => JSON.stringify({ version: '9.9.9' })); + + expect(resolvePackageVersion()).toBe('9.9.9'); + }); + + it('treats ENOTDIR as a missing manifest', () => { + readFileSyncMock + .mockImplementationOnce(() => { + throw fsError('ENOTDIR'); + }) + .mockImplementationOnce(() => JSON.stringify({ version: '8.8.8' })); + + expect(resolvePackageVersion()).toBe('8.8.8'); + }); + + it('skips a manifest that has no version field', () => { + readFileSyncMock + .mockImplementationOnce(() => JSON.stringify({ name: 'no-version-here' })) + .mockImplementationOnce(() => JSON.stringify({ version: '7.7.7' })); + + expect(resolvePackageVersion()).toBe('7.7.7'); + }); + + it('stops searching at the package root', () => { + readFileSyncMock.mockImplementation(() => { + throw fsError('ENOENT'); + }); + + resolvePackageVersion(); + + expect(readFileSyncMock).toHaveBeenCalledTimes(2); + }); + + it('propagates errors other than a missing manifest', () => { + const denied = fsError('EACCES'); + readFileSyncMock.mockImplementation(() => { + throw denied; + }); + + expect(() => resolvePackageVersion()).toThrow(denied); + }); + + it('propagates a malformed manifest', () => { + readFileSyncMock.mockImplementation(() => '{ "version": '); + + expect(() => resolvePackageVersion()).toThrow(SyntaxError); + }); +}); From be924e48f70463375ee8d96b241a1bc82c9d1ebb Mon Sep 17 00:00:00 2001 From: Ahmed Saad Date: Tue, 4 Aug 2026 11:25:51 +0300 Subject: [PATCH 5/6] refactor: align version resolution with the pattern in #4576 --- src/everything/__tests__/version.test.ts | 124 +++++++++++------------ src/everything/server/index.ts | 4 +- src/everything/version.ts | 49 ++++----- src/filesystem/__tests__/version.test.ts | 124 +++++++++++------------ src/filesystem/index.ts | 4 +- src/filesystem/version.ts | 49 ++++----- src/memory/__tests__/version.test.ts | 124 +++++++++++------------ src/memory/index.ts | 4 +- src/memory/version.ts | 49 ++++----- 9 files changed, 264 insertions(+), 267 deletions(-) diff --git a/src/everything/__tests__/version.test.ts b/src/everything/__tests__/version.test.ts index 62bb97b659..c66f504f34 100644 --- a/src/everything/__tests__/version.test.ts +++ b/src/everything/__tests__/version.test.ts @@ -1,26 +1,32 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { readFileSync } from 'node:fs'; -import { dirname, join } from 'node:path'; +import { createRequire } from 'node:module'; +import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { resolvePackageVersion } from '../version.js'; +import { resolvePackageVersion, SERVER_VERSION } from '../version.js'; -vi.mock('node:fs', async (importOriginal) => { - const actual = await importOriginal(); - return { ...actual, readFileSync: vi.fn(actual.readFileSync) }; +vi.mock('node:module', async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, createRequire: vi.fn(actual.createRequire) }; }); -const actualFs = await vi.importActual('node:fs'); -const readFileSyncMock = vi.mocked(readFileSync); +const actualModule = await vi.importActual('node:module'); +const createRequireMock = vi.mocked(createRequire); -const packageRoot = dirname(dirname(fileURLToPath(import.meta.url))); -const { version } = JSON.parse(actualFs.readFileSync(join(packageRoot, 'package.json'), 'utf8')); +const packageRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url))); +const { version } = JSON.parse(readFileSync(path.join(packageRoot, 'package.json'), 'utf8')); -/** fs errors carry a `code`; only ENOENT/ENOTDIR mean "not at this path". */ -const fsError = (code: string) => Object.assign(new Error(code), { code }); +/** A `require` that fails to find a module carries this code; anything else is a real failure. */ +const moduleNotFound = () => + Object.assign(new Error('Cannot find module'), { code: 'MODULE_NOT_FOUND' }); + +/** Stands in for the `require` returned by createRequire, driven by `impl`. */ +const stubRequire = (impl: (id: string) => unknown) => + impl as unknown as ReturnType; beforeEach(() => { - readFileSyncMock.mockReset(); - readFileSyncMock.mockImplementation(actualFs.readFileSync); + createRequireMock.mockReset(); + createRequireMock.mockImplementation(actualModule.createRequire); }); describe('resolvePackageVersion', () => { @@ -28,75 +34,65 @@ describe('resolvePackageVersion', () => { expect(resolvePackageVersion()).toBe(version); }); - it('does not fall back while the manifest is readable', () => { - expect(resolvePackageVersion('unused-fallback')).toBe(version); - }); - - it('falls back when no manifest is found', () => { - readFileSyncMock.mockImplementation(() => { - throw fsError('ENOENT'); - }); - - expect(resolvePackageVersion('1.2.3-fallback')).toBe('1.2.3-fallback'); - }); - - it('falls back to 0.0.0-dev by default', () => { - readFileSyncMock.mockImplementation(() => { - throw fsError('ENOENT'); - }); - - expect(resolvePackageVersion()).toBe('0.0.0-dev'); + it('exposes the resolved version as SERVER_VERSION', () => { + expect(SERVER_VERSION).toBe(version); }); - it('checks the parent directory, covering the dist/ layout', () => { - readFileSyncMock - .mockImplementationOnce(() => { - throw fsError('ENOENT'); - }) - .mockImplementationOnce(() => JSON.stringify({ version: '9.9.9' })); + it('falls through to the parent directory, covering the dist/ layout', () => { + const seen: string[] = []; + createRequireMock.mockReturnValue( + stubRequire((id) => { + seen.push(id); + if (seen.length === 1) throw moduleNotFound(); + return { version: '9.9.9' }; + }), + ); expect(resolvePackageVersion()).toBe('9.9.9'); - }); - - it('treats ENOTDIR as a missing manifest', () => { - readFileSyncMock - .mockImplementationOnce(() => { - throw fsError('ENOTDIR'); - }) - .mockImplementationOnce(() => JSON.stringify({ version: '8.8.8' })); - - expect(resolvePackageVersion()).toBe('8.8.8'); + expect(seen).toHaveLength(2); }); it('skips a manifest that has no version field', () => { - readFileSyncMock - .mockImplementationOnce(() => JSON.stringify({ name: 'no-version-here' })) - .mockImplementationOnce(() => JSON.stringify({ version: '7.7.7' })); + let call = 0; + createRequireMock.mockReturnValue( + stubRequire(() => (++call === 1 ? { name: 'no-version-here' } : { version: '7.7.7' })), + ); expect(resolvePackageVersion()).toBe('7.7.7'); }); - it('stops searching at the package root', () => { - readFileSyncMock.mockImplementation(() => { - throw fsError('ENOENT'); - }); - - resolvePackageVersion(); - - expect(readFileSyncMock).toHaveBeenCalledTimes(2); + it('throws when no manifest is found, without searching past the package root', () => { + const seen: string[] = []; + createRequireMock.mockReturnValue( + stubRequire((id) => { + seen.push(id); + throw moduleNotFound(); + }), + ); + + expect(() => resolvePackageVersion()).toThrow( + 'Could not locate package.json for server version', + ); + expect(seen).toHaveLength(2); }); it('propagates errors other than a missing manifest', () => { - const denied = fsError('EACCES'); - readFileSyncMock.mockImplementation(() => { - throw denied; - }); + const denied = Object.assign(new Error('permission denied'), { code: 'EACCES' }); + createRequireMock.mockReturnValue( + stubRequire(() => { + throw denied; + }), + ); expect(() => resolvePackageVersion()).toThrow(denied); }); - it('propagates a malformed manifest', () => { - readFileSyncMock.mockImplementation(() => '{ "version": '); + it('propagates a malformed manifest instead of reporting it as missing', () => { + createRequireMock.mockReturnValue( + stubRequire(() => { + throw new SyntaxError('Unexpected end of JSON input'); + }), + ); expect(() => resolvePackageVersion()).toThrow(SyntaxError); }); diff --git a/src/everything/server/index.ts b/src/everything/server/index.ts index a03186f5a3..b39897d611 100644 --- a/src/everything/server/index.ts +++ b/src/everything/server/index.ts @@ -12,7 +12,7 @@ import { registerResources, readInstructions } from "../resources/index.js"; import { registerPrompts } from "../prompts/index.js"; import { stopSimulatedLogging } from "./logging.js"; import { syncRoots } from "./roots.js"; -import { resolvePackageVersion } from "../version.js"; +import { SERVER_VERSION } from "../version.js"; // Server Factory response export type ServerFactoryResponse = { @@ -48,7 +48,7 @@ export const createServer: () => ServerFactoryResponse = () => { { name: "mcp-servers/everything", title: "Everything Reference Server", - version: resolvePackageVersion(), + version: SERVER_VERSION, }, { capabilities: { diff --git a/src/everything/version.ts b/src/everything/version.ts index 5dde4b3a94..043006cd7a 100644 --- a/src/everything/version.ts +++ b/src/everything/version.ts @@ -1,33 +1,36 @@ -import { readFileSync } from "node:fs"; -import { dirname, join } from "node:path"; +import { createRequire } from "node:module"; +import path from "node:path"; import { fileURLToPath } from "node:url"; -/** Only "manifest isn't here" is skippable; other errors propagate. */ -const isMissingFile = (error: unknown): boolean => { - const code = (error as NodeJS.ErrnoException)?.code; - return code === "ENOENT" || code === "ENOTDIR"; -}; - /** - * Reads this package's version from package.json. release.py stamps the version - * at release time, so a literal in source is always stale. + * Resolve this package's version from package.json. + * + * Works both from source (`src/everything/`) and from the published + * layout (`dist/`), where package.json lives one directory up. */ -export const resolvePackageVersion = (fallback = "0.0.0-dev"): string => { - const moduleDir = dirname(fileURLToPath(import.meta.url)); +export function resolvePackageVersion(): string { + const require = createRequire(import.meta.url); + const moduleDir = path.dirname(fileURLToPath(import.meta.url)); + const candidates = [ + path.join(moduleDir, "package.json"), + path.join(moduleDir, "..", "package.json"), + ]; - // Manifest sits alongside this module from source, one level up from dist/. - for (const dir of [moduleDir, dirname(moduleDir)]) { - let manifest: string; + for (const candidate of candidates) { try { - manifest = readFileSync(join(dir, "package.json"), "utf8"); + const pkg = require(candidate) as { version?: string }; + if (pkg.version) { + return pkg.version; + } } catch (error) { - if (isMissingFile(error)) continue; - throw error; + // Only a missing manifest is skippable; a corrupt or unreadable one is a real failure. + if ((error as NodeJS.ErrnoException)?.code !== "MODULE_NOT_FOUND") { + throw error; + } } - - const { version } = JSON.parse(manifest); - if (typeof version === "string") return version; } - return fallback; -}; + throw new Error("Could not locate package.json for server version"); +} + +export const SERVER_VERSION = resolvePackageVersion(); diff --git a/src/filesystem/__tests__/version.test.ts b/src/filesystem/__tests__/version.test.ts index 62bb97b659..c66f504f34 100644 --- a/src/filesystem/__tests__/version.test.ts +++ b/src/filesystem/__tests__/version.test.ts @@ -1,26 +1,32 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { readFileSync } from 'node:fs'; -import { dirname, join } from 'node:path'; +import { createRequire } from 'node:module'; +import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { resolvePackageVersion } from '../version.js'; +import { resolvePackageVersion, SERVER_VERSION } from '../version.js'; -vi.mock('node:fs', async (importOriginal) => { - const actual = await importOriginal(); - return { ...actual, readFileSync: vi.fn(actual.readFileSync) }; +vi.mock('node:module', async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, createRequire: vi.fn(actual.createRequire) }; }); -const actualFs = await vi.importActual('node:fs'); -const readFileSyncMock = vi.mocked(readFileSync); +const actualModule = await vi.importActual('node:module'); +const createRequireMock = vi.mocked(createRequire); -const packageRoot = dirname(dirname(fileURLToPath(import.meta.url))); -const { version } = JSON.parse(actualFs.readFileSync(join(packageRoot, 'package.json'), 'utf8')); +const packageRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url))); +const { version } = JSON.parse(readFileSync(path.join(packageRoot, 'package.json'), 'utf8')); -/** fs errors carry a `code`; only ENOENT/ENOTDIR mean "not at this path". */ -const fsError = (code: string) => Object.assign(new Error(code), { code }); +/** A `require` that fails to find a module carries this code; anything else is a real failure. */ +const moduleNotFound = () => + Object.assign(new Error('Cannot find module'), { code: 'MODULE_NOT_FOUND' }); + +/** Stands in for the `require` returned by createRequire, driven by `impl`. */ +const stubRequire = (impl: (id: string) => unknown) => + impl as unknown as ReturnType; beforeEach(() => { - readFileSyncMock.mockReset(); - readFileSyncMock.mockImplementation(actualFs.readFileSync); + createRequireMock.mockReset(); + createRequireMock.mockImplementation(actualModule.createRequire); }); describe('resolvePackageVersion', () => { @@ -28,75 +34,65 @@ describe('resolvePackageVersion', () => { expect(resolvePackageVersion()).toBe(version); }); - it('does not fall back while the manifest is readable', () => { - expect(resolvePackageVersion('unused-fallback')).toBe(version); - }); - - it('falls back when no manifest is found', () => { - readFileSyncMock.mockImplementation(() => { - throw fsError('ENOENT'); - }); - - expect(resolvePackageVersion('1.2.3-fallback')).toBe('1.2.3-fallback'); - }); - - it('falls back to 0.0.0-dev by default', () => { - readFileSyncMock.mockImplementation(() => { - throw fsError('ENOENT'); - }); - - expect(resolvePackageVersion()).toBe('0.0.0-dev'); + it('exposes the resolved version as SERVER_VERSION', () => { + expect(SERVER_VERSION).toBe(version); }); - it('checks the parent directory, covering the dist/ layout', () => { - readFileSyncMock - .mockImplementationOnce(() => { - throw fsError('ENOENT'); - }) - .mockImplementationOnce(() => JSON.stringify({ version: '9.9.9' })); + it('falls through to the parent directory, covering the dist/ layout', () => { + const seen: string[] = []; + createRequireMock.mockReturnValue( + stubRequire((id) => { + seen.push(id); + if (seen.length === 1) throw moduleNotFound(); + return { version: '9.9.9' }; + }), + ); expect(resolvePackageVersion()).toBe('9.9.9'); - }); - - it('treats ENOTDIR as a missing manifest', () => { - readFileSyncMock - .mockImplementationOnce(() => { - throw fsError('ENOTDIR'); - }) - .mockImplementationOnce(() => JSON.stringify({ version: '8.8.8' })); - - expect(resolvePackageVersion()).toBe('8.8.8'); + expect(seen).toHaveLength(2); }); it('skips a manifest that has no version field', () => { - readFileSyncMock - .mockImplementationOnce(() => JSON.stringify({ name: 'no-version-here' })) - .mockImplementationOnce(() => JSON.stringify({ version: '7.7.7' })); + let call = 0; + createRequireMock.mockReturnValue( + stubRequire(() => (++call === 1 ? { name: 'no-version-here' } : { version: '7.7.7' })), + ); expect(resolvePackageVersion()).toBe('7.7.7'); }); - it('stops searching at the package root', () => { - readFileSyncMock.mockImplementation(() => { - throw fsError('ENOENT'); - }); - - resolvePackageVersion(); - - expect(readFileSyncMock).toHaveBeenCalledTimes(2); + it('throws when no manifest is found, without searching past the package root', () => { + const seen: string[] = []; + createRequireMock.mockReturnValue( + stubRequire((id) => { + seen.push(id); + throw moduleNotFound(); + }), + ); + + expect(() => resolvePackageVersion()).toThrow( + 'Could not locate package.json for server version', + ); + expect(seen).toHaveLength(2); }); it('propagates errors other than a missing manifest', () => { - const denied = fsError('EACCES'); - readFileSyncMock.mockImplementation(() => { - throw denied; - }); + const denied = Object.assign(new Error('permission denied'), { code: 'EACCES' }); + createRequireMock.mockReturnValue( + stubRequire(() => { + throw denied; + }), + ); expect(() => resolvePackageVersion()).toThrow(denied); }); - it('propagates a malformed manifest', () => { - readFileSyncMock.mockImplementation(() => '{ "version": '); + it('propagates a malformed manifest instead of reporting it as missing', () => { + createRequireMock.mockReturnValue( + stubRequire(() => { + throw new SyntaxError('Unexpected end of JSON input'); + }), + ); expect(() => resolvePackageVersion()).toThrow(SyntaxError); }); diff --git a/src/filesystem/index.ts b/src/filesystem/index.ts index 47f6e9d488..e7d5c91e20 100644 --- a/src/filesystem/index.ts +++ b/src/filesystem/index.ts @@ -27,7 +27,7 @@ import { headFile, setAllowedDirectories, } from './lib.js'; -import { resolvePackageVersion } from './version.js'; +import { SERVER_VERSION } from './version.js'; // Command line argument parsing const args = process.argv.slice(2); @@ -164,7 +164,7 @@ const GetFileInfoArgsSchema = z.object({ const server = new McpServer( { name: "secure-filesystem-server", - version: resolvePackageVersion(), + version: SERVER_VERSION, } ); diff --git a/src/filesystem/version.ts b/src/filesystem/version.ts index b4aeb2b68a..5d97c4c3d2 100644 --- a/src/filesystem/version.ts +++ b/src/filesystem/version.ts @@ -1,33 +1,36 @@ -import { readFileSync } from 'node:fs'; -import { dirname, join } from 'node:path'; +import { createRequire } from 'node:module'; +import path from 'node:path'; import { fileURLToPath } from 'node:url'; -/** Only "manifest isn't here" is skippable; other errors propagate. */ -const isMissingFile = (error: unknown): boolean => { - const code = (error as NodeJS.ErrnoException)?.code; - return code === 'ENOENT' || code === 'ENOTDIR'; -}; - /** - * Reads this package's version from package.json. release.py stamps the version - * at release time, so a literal in source is always stale. + * Resolve this package's version from package.json. + * + * Works both from source (`src/filesystem/`) and from the published + * layout (`dist/`), where package.json lives one directory up. */ -export const resolvePackageVersion = (fallback = '0.0.0-dev'): string => { - const moduleDir = dirname(fileURLToPath(import.meta.url)); +export function resolvePackageVersion(): string { + const require = createRequire(import.meta.url); + const moduleDir = path.dirname(fileURLToPath(import.meta.url)); + const candidates = [ + path.join(moduleDir, 'package.json'), + path.join(moduleDir, '..', 'package.json'), + ]; - // Manifest sits alongside this module from source, one level up from dist/. - for (const dir of [moduleDir, dirname(moduleDir)]) { - let manifest: string; + for (const candidate of candidates) { try { - manifest = readFileSync(join(dir, 'package.json'), 'utf8'); + const pkg = require(candidate) as { version?: string }; + if (pkg.version) { + return pkg.version; + } } catch (error) { - if (isMissingFile(error)) continue; - throw error; + // Only a missing manifest is skippable; a corrupt or unreadable one is a real failure. + if ((error as NodeJS.ErrnoException)?.code !== 'MODULE_NOT_FOUND') { + throw error; + } } - - const { version } = JSON.parse(manifest); - if (typeof version === 'string') return version; } - return fallback; -}; + throw new Error('Could not locate package.json for server version'); +} + +export const SERVER_VERSION = resolvePackageVersion(); diff --git a/src/memory/__tests__/version.test.ts b/src/memory/__tests__/version.test.ts index 62bb97b659..c66f504f34 100644 --- a/src/memory/__tests__/version.test.ts +++ b/src/memory/__tests__/version.test.ts @@ -1,26 +1,32 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { readFileSync } from 'node:fs'; -import { dirname, join } from 'node:path'; +import { createRequire } from 'node:module'; +import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { resolvePackageVersion } from '../version.js'; +import { resolvePackageVersion, SERVER_VERSION } from '../version.js'; -vi.mock('node:fs', async (importOriginal) => { - const actual = await importOriginal(); - return { ...actual, readFileSync: vi.fn(actual.readFileSync) }; +vi.mock('node:module', async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, createRequire: vi.fn(actual.createRequire) }; }); -const actualFs = await vi.importActual('node:fs'); -const readFileSyncMock = vi.mocked(readFileSync); +const actualModule = await vi.importActual('node:module'); +const createRequireMock = vi.mocked(createRequire); -const packageRoot = dirname(dirname(fileURLToPath(import.meta.url))); -const { version } = JSON.parse(actualFs.readFileSync(join(packageRoot, 'package.json'), 'utf8')); +const packageRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url))); +const { version } = JSON.parse(readFileSync(path.join(packageRoot, 'package.json'), 'utf8')); -/** fs errors carry a `code`; only ENOENT/ENOTDIR mean "not at this path". */ -const fsError = (code: string) => Object.assign(new Error(code), { code }); +/** A `require` that fails to find a module carries this code; anything else is a real failure. */ +const moduleNotFound = () => + Object.assign(new Error('Cannot find module'), { code: 'MODULE_NOT_FOUND' }); + +/** Stands in for the `require` returned by createRequire, driven by `impl`. */ +const stubRequire = (impl: (id: string) => unknown) => + impl as unknown as ReturnType; beforeEach(() => { - readFileSyncMock.mockReset(); - readFileSyncMock.mockImplementation(actualFs.readFileSync); + createRequireMock.mockReset(); + createRequireMock.mockImplementation(actualModule.createRequire); }); describe('resolvePackageVersion', () => { @@ -28,75 +34,65 @@ describe('resolvePackageVersion', () => { expect(resolvePackageVersion()).toBe(version); }); - it('does not fall back while the manifest is readable', () => { - expect(resolvePackageVersion('unused-fallback')).toBe(version); - }); - - it('falls back when no manifest is found', () => { - readFileSyncMock.mockImplementation(() => { - throw fsError('ENOENT'); - }); - - expect(resolvePackageVersion('1.2.3-fallback')).toBe('1.2.3-fallback'); - }); - - it('falls back to 0.0.0-dev by default', () => { - readFileSyncMock.mockImplementation(() => { - throw fsError('ENOENT'); - }); - - expect(resolvePackageVersion()).toBe('0.0.0-dev'); + it('exposes the resolved version as SERVER_VERSION', () => { + expect(SERVER_VERSION).toBe(version); }); - it('checks the parent directory, covering the dist/ layout', () => { - readFileSyncMock - .mockImplementationOnce(() => { - throw fsError('ENOENT'); - }) - .mockImplementationOnce(() => JSON.stringify({ version: '9.9.9' })); + it('falls through to the parent directory, covering the dist/ layout', () => { + const seen: string[] = []; + createRequireMock.mockReturnValue( + stubRequire((id) => { + seen.push(id); + if (seen.length === 1) throw moduleNotFound(); + return { version: '9.9.9' }; + }), + ); expect(resolvePackageVersion()).toBe('9.9.9'); - }); - - it('treats ENOTDIR as a missing manifest', () => { - readFileSyncMock - .mockImplementationOnce(() => { - throw fsError('ENOTDIR'); - }) - .mockImplementationOnce(() => JSON.stringify({ version: '8.8.8' })); - - expect(resolvePackageVersion()).toBe('8.8.8'); + expect(seen).toHaveLength(2); }); it('skips a manifest that has no version field', () => { - readFileSyncMock - .mockImplementationOnce(() => JSON.stringify({ name: 'no-version-here' })) - .mockImplementationOnce(() => JSON.stringify({ version: '7.7.7' })); + let call = 0; + createRequireMock.mockReturnValue( + stubRequire(() => (++call === 1 ? { name: 'no-version-here' } : { version: '7.7.7' })), + ); expect(resolvePackageVersion()).toBe('7.7.7'); }); - it('stops searching at the package root', () => { - readFileSyncMock.mockImplementation(() => { - throw fsError('ENOENT'); - }); - - resolvePackageVersion(); - - expect(readFileSyncMock).toHaveBeenCalledTimes(2); + it('throws when no manifest is found, without searching past the package root', () => { + const seen: string[] = []; + createRequireMock.mockReturnValue( + stubRequire((id) => { + seen.push(id); + throw moduleNotFound(); + }), + ); + + expect(() => resolvePackageVersion()).toThrow( + 'Could not locate package.json for server version', + ); + expect(seen).toHaveLength(2); }); it('propagates errors other than a missing manifest', () => { - const denied = fsError('EACCES'); - readFileSyncMock.mockImplementation(() => { - throw denied; - }); + const denied = Object.assign(new Error('permission denied'), { code: 'EACCES' }); + createRequireMock.mockReturnValue( + stubRequire(() => { + throw denied; + }), + ); expect(() => resolvePackageVersion()).toThrow(denied); }); - it('propagates a malformed manifest', () => { - readFileSyncMock.mockImplementation(() => '{ "version": '); + it('propagates a malformed manifest instead of reporting it as missing', () => { + createRequireMock.mockReturnValue( + stubRequire(() => { + throw new SyntaxError('Unexpected end of JSON input'); + }), + ); expect(() => resolvePackageVersion()).toThrow(SyntaxError); }); diff --git a/src/memory/index.ts b/src/memory/index.ts index b2ce217833..ce17f4de6e 100644 --- a/src/memory/index.ts +++ b/src/memory/index.ts @@ -7,7 +7,7 @@ import { z } from "zod"; import { promises as fs } from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; -import { resolvePackageVersion } from './version.js'; +import { SERVER_VERSION } from './version.js'; // Define memory file path using environment variable with fallback export const defaultMemoryPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'memory.jsonl'); @@ -257,7 +257,7 @@ const RelationSchema = z.object({ // The server instance and tools exposed to Claude const server = new McpServer({ name: "memory-server", - version: resolvePackageVersion(), + version: SERVER_VERSION, }); const RESOURCE_URI = "memory://knowledge-graph"; diff --git a/src/memory/version.ts b/src/memory/version.ts index b4aeb2b68a..73af44ce41 100644 --- a/src/memory/version.ts +++ b/src/memory/version.ts @@ -1,33 +1,36 @@ -import { readFileSync } from 'node:fs'; -import { dirname, join } from 'node:path'; +import { createRequire } from 'node:module'; +import path from 'node:path'; import { fileURLToPath } from 'node:url'; -/** Only "manifest isn't here" is skippable; other errors propagate. */ -const isMissingFile = (error: unknown): boolean => { - const code = (error as NodeJS.ErrnoException)?.code; - return code === 'ENOENT' || code === 'ENOTDIR'; -}; - /** - * Reads this package's version from package.json. release.py stamps the version - * at release time, so a literal in source is always stale. + * Resolve this package's version from package.json. + * + * Works both from source (`src/memory/`) and from the published + * layout (`dist/`), where package.json lives one directory up. */ -export const resolvePackageVersion = (fallback = '0.0.0-dev'): string => { - const moduleDir = dirname(fileURLToPath(import.meta.url)); +export function resolvePackageVersion(): string { + const require = createRequire(import.meta.url); + const moduleDir = path.dirname(fileURLToPath(import.meta.url)); + const candidates = [ + path.join(moduleDir, 'package.json'), + path.join(moduleDir, '..', 'package.json'), + ]; - // Manifest sits alongside this module from source, one level up from dist/. - for (const dir of [moduleDir, dirname(moduleDir)]) { - let manifest: string; + for (const candidate of candidates) { try { - manifest = readFileSync(join(dir, 'package.json'), 'utf8'); + const pkg = require(candidate) as { version?: string }; + if (pkg.version) { + return pkg.version; + } } catch (error) { - if (isMissingFile(error)) continue; - throw error; + // Only a missing manifest is skippable; a corrupt or unreadable one is a real failure. + if ((error as NodeJS.ErrnoException)?.code !== 'MODULE_NOT_FOUND') { + throw error; + } } - - const { version } = JSON.parse(manifest); - if (typeof version === 'string') return version; } - return fallback; -}; + throw new Error('Could not locate package.json for server version'); +} + +export const SERVER_VERSION = resolvePackageVersion(); From 4b60dbdad9fe2093be448ce45186270f1dc36243 Mon Sep 17 00:00:00 2001 From: Ahmed Saad Date: Tue, 4 Aug 2026 11:58:26 +0300 Subject: [PATCH 6/6] test: cover the built artifact and trim redundant version cases --- src/everything/__tests__/version.test.ts | 67 +++++++++++++----------- src/filesystem/__tests__/version.test.ts | 67 +++++++++++++----------- src/memory/__tests__/version.test.ts | 67 +++++++++++++----------- 3 files changed, 111 insertions(+), 90 deletions(-) diff --git a/src/everything/__tests__/version.test.ts b/src/everything/__tests__/version.test.ts index c66f504f34..60d826428b 100644 --- a/src/everything/__tests__/version.test.ts +++ b/src/everything/__tests__/version.test.ts @@ -1,9 +1,11 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { readFileSync } from 'node:fs'; +import { existsSync, readFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { resolvePackageVersion, SERVER_VERSION } from '../version.js'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; +import { resolvePackageVersion } from '../version.js'; vi.mock('node:module', async (importOriginal) => { const actual = await importOriginal(); @@ -34,33 +36,6 @@ describe('resolvePackageVersion', () => { expect(resolvePackageVersion()).toBe(version); }); - it('exposes the resolved version as SERVER_VERSION', () => { - expect(SERVER_VERSION).toBe(version); - }); - - it('falls through to the parent directory, covering the dist/ layout', () => { - const seen: string[] = []; - createRequireMock.mockReturnValue( - stubRequire((id) => { - seen.push(id); - if (seen.length === 1) throw moduleNotFound(); - return { version: '9.9.9' }; - }), - ); - - expect(resolvePackageVersion()).toBe('9.9.9'); - expect(seen).toHaveLength(2); - }); - - it('skips a manifest that has no version field', () => { - let call = 0; - createRequireMock.mockReturnValue( - stubRequire(() => (++call === 1 ? { name: 'no-version-here' } : { version: '7.7.7' })), - ); - - expect(resolvePackageVersion()).toBe('7.7.7'); - }); - it('throws when no manifest is found, without searching past the package root', () => { const seen: string[] = []; createRequireMock.mockReturnValue( @@ -97,3 +72,35 @@ describe('resolvePackageVersion', () => { expect(() => resolvePackageVersion()).toThrow(SyntaxError); }); }); + +// The cases above drive the resolver directly; these exercise the real build. +// They skip when dist/ is absent so an unbuilt tree still passes. +const distVersionPath = path.join(packageRoot, 'dist', 'version.js'); +const distIndexPath = path.join(packageRoot, 'dist', 'index.js'); + +describe('built output', () => { + it.skipIf(!existsSync(distVersionPath))( + 'resolves package.json from the dist layout after build', + async () => { + const dist = await import(/* @vite-ignore */ pathToFileURL(distVersionPath).href); + + expect(dist.SERVER_VERSION).toBe(version); + }, + ); + + it.skipIf(!existsSync(distIndexPath))( + 'stdio initialize reports package.json version in serverInfo', + async () => { + const client = new Client({ name: 'version-test', version: '1.0.0' }, { capabilities: {} }); + await client.connect( + new StdioClientTransport({ command: process.execPath, args: [distIndexPath] }), + ); + + try { + expect(client.getServerVersion()?.version).toBe(version); + } finally { + await client.close(); + } + }, + ); +}); diff --git a/src/filesystem/__tests__/version.test.ts b/src/filesystem/__tests__/version.test.ts index c66f504f34..60d826428b 100644 --- a/src/filesystem/__tests__/version.test.ts +++ b/src/filesystem/__tests__/version.test.ts @@ -1,9 +1,11 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { readFileSync } from 'node:fs'; +import { existsSync, readFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { resolvePackageVersion, SERVER_VERSION } from '../version.js'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; +import { resolvePackageVersion } from '../version.js'; vi.mock('node:module', async (importOriginal) => { const actual = await importOriginal(); @@ -34,33 +36,6 @@ describe('resolvePackageVersion', () => { expect(resolvePackageVersion()).toBe(version); }); - it('exposes the resolved version as SERVER_VERSION', () => { - expect(SERVER_VERSION).toBe(version); - }); - - it('falls through to the parent directory, covering the dist/ layout', () => { - const seen: string[] = []; - createRequireMock.mockReturnValue( - stubRequire((id) => { - seen.push(id); - if (seen.length === 1) throw moduleNotFound(); - return { version: '9.9.9' }; - }), - ); - - expect(resolvePackageVersion()).toBe('9.9.9'); - expect(seen).toHaveLength(2); - }); - - it('skips a manifest that has no version field', () => { - let call = 0; - createRequireMock.mockReturnValue( - stubRequire(() => (++call === 1 ? { name: 'no-version-here' } : { version: '7.7.7' })), - ); - - expect(resolvePackageVersion()).toBe('7.7.7'); - }); - it('throws when no manifest is found, without searching past the package root', () => { const seen: string[] = []; createRequireMock.mockReturnValue( @@ -97,3 +72,35 @@ describe('resolvePackageVersion', () => { expect(() => resolvePackageVersion()).toThrow(SyntaxError); }); }); + +// The cases above drive the resolver directly; these exercise the real build. +// They skip when dist/ is absent so an unbuilt tree still passes. +const distVersionPath = path.join(packageRoot, 'dist', 'version.js'); +const distIndexPath = path.join(packageRoot, 'dist', 'index.js'); + +describe('built output', () => { + it.skipIf(!existsSync(distVersionPath))( + 'resolves package.json from the dist layout after build', + async () => { + const dist = await import(/* @vite-ignore */ pathToFileURL(distVersionPath).href); + + expect(dist.SERVER_VERSION).toBe(version); + }, + ); + + it.skipIf(!existsSync(distIndexPath))( + 'stdio initialize reports package.json version in serverInfo', + async () => { + const client = new Client({ name: 'version-test', version: '1.0.0' }, { capabilities: {} }); + await client.connect( + new StdioClientTransport({ command: process.execPath, args: [distIndexPath] }), + ); + + try { + expect(client.getServerVersion()?.version).toBe(version); + } finally { + await client.close(); + } + }, + ); +}); diff --git a/src/memory/__tests__/version.test.ts b/src/memory/__tests__/version.test.ts index c66f504f34..60d826428b 100644 --- a/src/memory/__tests__/version.test.ts +++ b/src/memory/__tests__/version.test.ts @@ -1,9 +1,11 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { readFileSync } from 'node:fs'; +import { existsSync, readFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { resolvePackageVersion, SERVER_VERSION } from '../version.js'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; +import { resolvePackageVersion } from '../version.js'; vi.mock('node:module', async (importOriginal) => { const actual = await importOriginal(); @@ -34,33 +36,6 @@ describe('resolvePackageVersion', () => { expect(resolvePackageVersion()).toBe(version); }); - it('exposes the resolved version as SERVER_VERSION', () => { - expect(SERVER_VERSION).toBe(version); - }); - - it('falls through to the parent directory, covering the dist/ layout', () => { - const seen: string[] = []; - createRequireMock.mockReturnValue( - stubRequire((id) => { - seen.push(id); - if (seen.length === 1) throw moduleNotFound(); - return { version: '9.9.9' }; - }), - ); - - expect(resolvePackageVersion()).toBe('9.9.9'); - expect(seen).toHaveLength(2); - }); - - it('skips a manifest that has no version field', () => { - let call = 0; - createRequireMock.mockReturnValue( - stubRequire(() => (++call === 1 ? { name: 'no-version-here' } : { version: '7.7.7' })), - ); - - expect(resolvePackageVersion()).toBe('7.7.7'); - }); - it('throws when no manifest is found, without searching past the package root', () => { const seen: string[] = []; createRequireMock.mockReturnValue( @@ -97,3 +72,35 @@ describe('resolvePackageVersion', () => { expect(() => resolvePackageVersion()).toThrow(SyntaxError); }); }); + +// The cases above drive the resolver directly; these exercise the real build. +// They skip when dist/ is absent so an unbuilt tree still passes. +const distVersionPath = path.join(packageRoot, 'dist', 'version.js'); +const distIndexPath = path.join(packageRoot, 'dist', 'index.js'); + +describe('built output', () => { + it.skipIf(!existsSync(distVersionPath))( + 'resolves package.json from the dist layout after build', + async () => { + const dist = await import(/* @vite-ignore */ pathToFileURL(distVersionPath).href); + + expect(dist.SERVER_VERSION).toBe(version); + }, + ); + + it.skipIf(!existsSync(distIndexPath))( + 'stdio initialize reports package.json version in serverInfo', + async () => { + const client = new Client({ name: 'version-test', version: '1.0.0' }, { capabilities: {} }); + await client.connect( + new StdioClientTransport({ command: process.execPath, args: [distIndexPath] }), + ); + + try { + expect(client.getServerVersion()?.version).toBe(version); + } finally { + await client.close(); + } + }, + ); +});