diff --git a/src/everything/__tests__/version.test.ts b/src/everything/__tests__/version.test.ts new file mode 100644 index 0000000000..60d826428b --- /dev/null +++ b/src/everything/__tests__/version.test.ts @@ -0,0 +1,106 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { existsSync, readFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import path from 'node:path'; +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(); + return { ...actual, createRequire: vi.fn(actual.createRequire) }; +}); + +const actualModule = await vi.importActual('node:module'); +const createRequireMock = vi.mocked(createRequire); + +const packageRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url))); +const { version } = JSON.parse(readFileSync(path.join(packageRoot, 'package.json'), 'utf8')); + +/** 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(() => { + createRequireMock.mockReset(); + createRequireMock.mockImplementation(actualModule.createRequire); +}); + +describe('resolvePackageVersion', () => { + it('reports the version from package.json', () => { + expect(resolvePackageVersion()).toBe(version); + }); + + 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 = Object.assign(new Error('permission denied'), { code: 'EACCES' }); + createRequireMock.mockReturnValue( + stubRequire(() => { + throw denied; + }), + ); + + expect(() => resolvePackageVersion()).toThrow(denied); + }); + + 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); + }); +}); + +// 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/everything/server/index.ts b/src/everything/server/index.ts index f1459cc812..b39897d611 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 { SERVER_VERSION } 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: SERVER_VERSION, }, { capabilities: { diff --git a/src/everything/version.ts b/src/everything/version.ts new file mode 100644 index 0000000000..043006cd7a --- /dev/null +++ b/src/everything/version.ts @@ -0,0 +1,36 @@ +import { createRequire } from "node:module"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +/** + * 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 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"), + ]; + + for (const candidate of candidates) { + try { + const pkg = require(candidate) as { version?: string }; + if (pkg.version) { + return pkg.version; + } + } catch (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; + } + } + } + + 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 new file mode 100644 index 0000000000..60d826428b --- /dev/null +++ b/src/filesystem/__tests__/version.test.ts @@ -0,0 +1,106 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { existsSync, readFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import path from 'node:path'; +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(); + return { ...actual, createRequire: vi.fn(actual.createRequire) }; +}); + +const actualModule = await vi.importActual('node:module'); +const createRequireMock = vi.mocked(createRequire); + +const packageRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url))); +const { version } = JSON.parse(readFileSync(path.join(packageRoot, 'package.json'), 'utf8')); + +/** 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(() => { + createRequireMock.mockReset(); + createRequireMock.mockImplementation(actualModule.createRequire); +}); + +describe('resolvePackageVersion', () => { + it('reports the version from package.json', () => { + expect(resolvePackageVersion()).toBe(version); + }); + + 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 = Object.assign(new Error('permission denied'), { code: 'EACCES' }); + createRequireMock.mockReturnValue( + stubRequire(() => { + throw denied; + }), + ); + + expect(() => resolvePackageVersion()).toThrow(denied); + }); + + 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); + }); +}); + +// 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/index.ts b/src/filesystem/index.ts index 234605bb13..e7d5c91e20 100644 --- a/src/filesystem/index.ts +++ b/src/filesystem/index.ts @@ -27,6 +27,7 @@ import { headFile, setAllowedDirectories, } from './lib.js'; +import { SERVER_VERSION } 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: SERVER_VERSION, } ); diff --git a/src/filesystem/version.ts b/src/filesystem/version.ts new file mode 100644 index 0000000000..5d97c4c3d2 --- /dev/null +++ b/src/filesystem/version.ts @@ -0,0 +1,36 @@ +import { createRequire } from 'node:module'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +/** + * 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 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'), + ]; + + for (const candidate of candidates) { + try { + const pkg = require(candidate) as { version?: string }; + if (pkg.version) { + return pkg.version; + } + } catch (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; + } + } + } + + 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 new file mode 100644 index 0000000000..60d826428b --- /dev/null +++ b/src/memory/__tests__/version.test.ts @@ -0,0 +1,106 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { existsSync, readFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import path from 'node:path'; +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(); + return { ...actual, createRequire: vi.fn(actual.createRequire) }; +}); + +const actualModule = await vi.importActual('node:module'); +const createRequireMock = vi.mocked(createRequire); + +const packageRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url))); +const { version } = JSON.parse(readFileSync(path.join(packageRoot, 'package.json'), 'utf8')); + +/** 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(() => { + createRequireMock.mockReset(); + createRequireMock.mockImplementation(actualModule.createRequire); +}); + +describe('resolvePackageVersion', () => { + it('reports the version from package.json', () => { + expect(resolvePackageVersion()).toBe(version); + }); + + 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 = Object.assign(new Error('permission denied'), { code: 'EACCES' }); + createRequireMock.mockReturnValue( + stubRequire(() => { + throw denied; + }), + ); + + expect(() => resolvePackageVersion()).toThrow(denied); + }); + + 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); + }); +}); + +// 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/index.ts b/src/memory/index.ts index 9865c5318e..ce17f4de6e 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 { 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'); @@ -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: SERVER_VERSION, }); const RESOURCE_URI = "memory://knowledge-graph"; diff --git a/src/memory/version.ts b/src/memory/version.ts new file mode 100644 index 0000000000..73af44ce41 --- /dev/null +++ b/src/memory/version.ts @@ -0,0 +1,36 @@ +import { createRequire } from 'node:module'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +/** + * 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 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'), + ]; + + for (const candidate of candidates) { + try { + const pkg = require(candidate) as { version?: string }; + if (pkg.version) { + return pkg.version; + } + } catch (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; + } + } + } + + throw new Error('Could not locate package.json for server version'); +} + +export const SERVER_VERSION = resolvePackageVersion();