Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ coverage/
*.swo
*~
.DS_Store
._*

# Runtime
*.pid
Expand Down
2 changes: 1 addition & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const MAX_QUERY_DOCUMENTS_TOP_K = 20;
export const QUERY_DOCUMENTS_MAX_CHUNKS = 500;

export const SERVER_NAME = 'Pinecone Read-Only MCP';
export const SERVER_VERSION = '0.1.0';
export { SERVER_VERSION } from './server-version.js';

export const SERVER_INSTRUCTIONS = `A semantic search server that provides hybrid search capabilities over Pinecone vector indexes with automatic namespace discovery.

Expand Down
110 changes: 110 additions & 0 deletions src/server-version.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { readFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it, vi } from 'vitest';
import { parsePackageJsonVersion, resolveServerVersion, SERVER_VERSION } from './server-version.js';

function readRootPackageJson(): string {
const packageJsonPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
return readFileSync(packageJsonPath, 'utf8');
}

/**
* True when the server-reported version matches the package.json version.
* Returns false when the two strings differ (stale hardcoding or wrong file).
*/
function isServerVersionAligned(serverVersion: string, packageVersion: string): boolean {
return serverVersion === packageVersion;
}

/** Synthetic package.json bodies — only the `version` field matters for parsing. */
const PACKAGE_JSON_FIXTURES: readonly string[] = [
JSON.stringify({ name: 'a', version: '0.1.0' }),
JSON.stringify({ name: 'b', version: '1.0.0' }),
JSON.stringify({ version: '0.1.6', type: 'module' }),
JSON.stringify({ version: '2.3.4', private: true }),
];

describe('parsePackageJsonVersion', () => {
it('extracts version from several package.json shapes', () => {
expect(parsePackageJsonVersion(PACKAGE_JSON_FIXTURES[0])).toBe('0.1.0');
expect(parsePackageJsonVersion(PACKAGE_JSON_FIXTURES[1])).toBe('1.0.0');
expect(parsePackageJsonVersion(PACKAGE_JSON_FIXTURES[2])).toBe('0.1.6');
expect(parsePackageJsonVersion(PACKAGE_JSON_FIXTURES[3])).toBe('2.3.4');
});

it('trims surrounding whitespace on version', () => {
expect(parsePackageJsonVersion(JSON.stringify({ version: ' 1.2.3 ' }))).toBe('1.2.3');
});

it('returns default when version is only whitespace', () => {
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
try {
expect(parsePackageJsonVersion(JSON.stringify({ version: ' ' }))).toBe('0.0.1');
expect(errSpy).toHaveBeenCalled();
} finally {
errSpy.mockRestore();
}
});

it('returns default when version is not a string', () => {
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
try {
expect(parsePackageJsonVersion(JSON.stringify({ version: 1 }))).toBe('0.0.1');
expect(errSpy).toHaveBeenCalled();
} finally {
errSpy.mockRestore();
}
});
});

describe('isServerVersionAligned', () => {
it('returns true when server and package versions are the same string', () => {
for (const raw of PACKAGE_JSON_FIXTURES) {
const v = parsePackageJsonVersion(raw);
expect(isServerVersionAligned(v, v)).toBe(true);
}
});

it('returns false when server and package versions differ', () => {
expect(isServerVersionAligned('0.1.0', '0.2.0')).toBe(false);
expect(isServerVersionAligned('1.0.0', '2.0.0')).toBe(false);
});
});

describe('SERVER_VERSION', () => {
it('matches the root package.json version (live module read)', () => {
const packageVersion = parsePackageJsonVersion(readRootPackageJson());
expect(isServerVersionAligned(SERVER_VERSION, packageVersion)).toBe(true);
expect(SERVER_VERSION).toBe(packageVersion);
});
});

describe('resolveServerVersion', () => {
it('returns default version when package manifest path does not exist', () => {
const missing = join(tmpdir(), `no-package-json-${Date.now()}.json`);
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
try {
expect(resolveServerVersion(missing)).toBe('0.0.1');
expect(errSpy).toHaveBeenCalled();
} finally {
errSpy.mockRestore();
}
});

it('returns default version when path is missing even if npm_package_version is set', () => {
const missing = join(tmpdir(), `no-package-json-env-${Date.now()}.json`);
const prev = process.env.npm_package_version;
process.env.npm_package_version = '9.9.9-test';
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
try {
expect(resolveServerVersion(missing)).toBe('0.0.1');
expect(errSpy).toHaveBeenCalled();
} finally {
errSpy.mockRestore();
if (prev !== undefined) process.env.npm_package_version = prev;
else delete process.env.npm_package_version;
}
});
});
69 changes: 69 additions & 0 deletions src/server-version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* MCP server version — read from package.json next to the compiled output.
*/
import { existsSync, readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

const DEFAULT_SERVER_VERSION = '0.0.1';

const __dirname = dirname(fileURLToPath(import.meta.url));
const packageJsonPath = join(__dirname, '..', 'package.json');

/**
* Read `version` from package.json text (same rules as the live server).
* On invalid JSON, missing `version`, or invalid type, writes to stderr and returns {@link DEFAULT_SERVER_VERSION}.
*/
export function parsePackageJsonVersion(raw: string, pathForErrors = 'package.json'): string {
try {
const parsed = JSON.parse(raw) as { version?: unknown };
if (typeof parsed.version !== 'string') {
console.error(
`[server-version] invalid or missing "version" in ${pathForErrors}; using default ${DEFAULT_SERVER_VERSION}`
);
return DEFAULT_SERVER_VERSION;
}
const version = parsed.version.trim();
if (version.length === 0) {
console.error(
`[server-version] empty "version" in ${pathForErrors}; using default ${DEFAULT_SERVER_VERSION}`
);
return DEFAULT_SERVER_VERSION;
}
return version;
} catch (err) {
const detail = err instanceof Error ? err.message : String(err);
console.error(
`[server-version] could not parse ${pathForErrors} (${detail}); using default ${DEFAULT_SERVER_VERSION}`
);
return DEFAULT_SERVER_VERSION;
}
}

/**
* Resolve the MCP server version from the package manifest on disk (by default,
* `package.json` one directory above this module).
*
* @param overridePath - For tests; otherwise the repo root `package.json` next to compiled output.
*/
export function resolveServerVersion(overridePath?: string): string {
const packagePath = overridePath ?? packageJsonPath;
if (!existsSync(packagePath)) {
console.error(
`[server-version] package.json not found at ${packagePath}; using default ${DEFAULT_SERVER_VERSION}`
);
return DEFAULT_SERVER_VERSION;
}
try {
const raw = readFileSync(packagePath, 'utf8');
return parsePackageJsonVersion(raw, packagePath);
} catch (err) {
const detail = err instanceof Error ? err.message : String(err);
console.error(
`[server-version] could not read ${packagePath} (${detail}); using default ${DEFAULT_SERVER_VERSION}`
);
return DEFAULT_SERVER_VERSION;
}
}

export const SERVER_VERSION = resolveServerVersion();
Loading