Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions src/everything/__tests__/version.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof import('node:module')>();
return { ...actual, createRequire: vi.fn(actual.createRequire) };
});

const actualModule = await vi.importActual<typeof import('node:module')>('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<typeof createRequire>;

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();
}
},
);
});
3 changes: 2 additions & 1 deletion src/everything/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -47,7 +48,7 @@ export const createServer: () => ServerFactoryResponse = () => {
{
name: "mcp-servers/everything",
title: "Everything Reference Server",
version: "2.0.0",
version: SERVER_VERSION,
},
{
capabilities: {
Expand Down
36 changes: 36 additions & 0 deletions src/everything/version.ts
Original file line number Diff line number Diff line change
@@ -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();
106 changes: 106 additions & 0 deletions src/filesystem/__tests__/version.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof import('node:module')>();
return { ...actual, createRequire: vi.fn(actual.createRequire) };
});

const actualModule = await vi.importActual<typeof import('node:module')>('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<typeof createRequire>;

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();
}
},
);
});
3 changes: 2 additions & 1 deletion src/filesystem/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -163,7 +164,7 @@ const GetFileInfoArgsSchema = z.object({
const server = new McpServer(
{
name: "secure-filesystem-server",
version: "0.2.0",
version: SERVER_VERSION,
}
);

Expand Down
36 changes: 36 additions & 0 deletions src/filesystem/version.ts
Original file line number Diff line number Diff line change
@@ -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();
Loading