diff --git a/package-lock.json b/package-lock.json index 26261a7ade..83d862664b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3835,7 +3835,8 @@ "@modelcontextprotocol/sdk": "^1.29.0", "diff": "^8.0.3", "glob": "^10.5.0", - "minimatch": "^10.0.1" + "minimatch": "^10.0.1", + "zod": "^4.0.0" }, "bin": { "mcp-server-filesystem": "dist/index.js" @@ -3855,7 +3856,8 @@ "version": "0.6.3", "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@modelcontextprotocol/sdk": "^1.29.0" + "@modelcontextprotocol/sdk": "^1.29.0", + "zod": "^4.0.0" }, "bin": { "mcp-server-memory": "dist/index.js" @@ -3875,7 +3877,8 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", "chalk": "^5.3.0", - "yargs": "^17.7.2" + "yargs": "^17.7.2", + "zod": "^4.0.0" }, "bin": { "mcp-server-sequential-thinking": "dist/index.js" diff --git a/src/filesystem/__tests__/dependencies.test.ts b/src/filesystem/__tests__/dependencies.test.ts new file mode 100644 index 0000000000..5624195872 --- /dev/null +++ b/src/filesystem/__tests__/dependencies.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect } from 'vitest'; +import { readFileSync, readdirSync } from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { builtinModules } from 'module'; + +/** + * Guards against phantom dependencies: modules that are imported by the shipped + * source but not declared in package.json. Such imports resolve by accident + * under npm's hoisted node_modules layout, then fail at startup for anyone + * using a strict layout (pnpm, yarn PnP) with ERR_MODULE_NOT_FOUND. + */ +describe('declared dependencies', () => { + const packageDir = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); + + const packageJson = JSON.parse( + readFileSync(path.join(packageDir, 'package.json'), 'utf-8') + ) as { dependencies?: Record }; + + // Source files that get compiled into dist/, i.e. everything except tests and configs. + const sourceFiles = readdirSync(packageDir) + .filter((file) => file.endsWith('.ts')) + .filter((file) => !file.endsWith('.test.ts') && file !== 'vitest.config.ts'); + + const IMPORT_PATTERN = /(?:import|export)[\s\S]*?from\s*['"]([^'"]+)['"]|import\s*\(\s*['"]([^'"]+)['"]\s*\)/g; + + /** Reduces a specifier like `zod/v4` or `@scope/pkg/sub` to its package name. */ + const toPackageName = (specifier: string): string => { + const segments = specifier.split('/'); + return specifier.startsWith('@') ? segments.slice(0, 2).join('/') : segments[0]; + }; + + const isBareSpecifier = (specifier: string): boolean => + !specifier.startsWith('.') && + !specifier.startsWith('node:') && + !builtinModules.includes(toPackageName(specifier)); + + it('finds source files to check', () => { + expect(sourceFiles.length).toBeGreaterThan(0); + }); + + it.each(sourceFiles)('%s imports only declared packages', (file) => { + const contents = readFileSync(path.join(packageDir, file), 'utf-8'); + const declared = Object.keys(packageJson.dependencies ?? {}); + + const imported = new Set(); + for (const match of contents.matchAll(IMPORT_PATTERN)) { + const specifier = match[1] ?? match[2]; + if (specifier && isBareSpecifier(specifier)) { + imported.add(toPackageName(specifier)); + } + } + + expect(declared).toEqual(expect.arrayContaining([...imported])); + }); +}); diff --git a/src/filesystem/package.json b/src/filesystem/package.json index c943ce24d3..fbc8e58f8b 100644 --- a/src/filesystem/package.json +++ b/src/filesystem/package.json @@ -28,7 +28,8 @@ "@modelcontextprotocol/sdk": "^1.29.0", "diff": "^8.0.3", "glob": "^10.5.0", - "minimatch": "^10.0.1" + "minimatch": "^10.0.1", + "zod": "^4.0.0" }, "devDependencies": { "@types/diff": "^5.0.9", diff --git a/src/memory/__tests__/dependencies.test.ts b/src/memory/__tests__/dependencies.test.ts new file mode 100644 index 0000000000..5624195872 --- /dev/null +++ b/src/memory/__tests__/dependencies.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect } from 'vitest'; +import { readFileSync, readdirSync } from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { builtinModules } from 'module'; + +/** + * Guards against phantom dependencies: modules that are imported by the shipped + * source but not declared in package.json. Such imports resolve by accident + * under npm's hoisted node_modules layout, then fail at startup for anyone + * using a strict layout (pnpm, yarn PnP) with ERR_MODULE_NOT_FOUND. + */ +describe('declared dependencies', () => { + const packageDir = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); + + const packageJson = JSON.parse( + readFileSync(path.join(packageDir, 'package.json'), 'utf-8') + ) as { dependencies?: Record }; + + // Source files that get compiled into dist/, i.e. everything except tests and configs. + const sourceFiles = readdirSync(packageDir) + .filter((file) => file.endsWith('.ts')) + .filter((file) => !file.endsWith('.test.ts') && file !== 'vitest.config.ts'); + + const IMPORT_PATTERN = /(?:import|export)[\s\S]*?from\s*['"]([^'"]+)['"]|import\s*\(\s*['"]([^'"]+)['"]\s*\)/g; + + /** Reduces a specifier like `zod/v4` or `@scope/pkg/sub` to its package name. */ + const toPackageName = (specifier: string): string => { + const segments = specifier.split('/'); + return specifier.startsWith('@') ? segments.slice(0, 2).join('/') : segments[0]; + }; + + const isBareSpecifier = (specifier: string): boolean => + !specifier.startsWith('.') && + !specifier.startsWith('node:') && + !builtinModules.includes(toPackageName(specifier)); + + it('finds source files to check', () => { + expect(sourceFiles.length).toBeGreaterThan(0); + }); + + it.each(sourceFiles)('%s imports only declared packages', (file) => { + const contents = readFileSync(path.join(packageDir, file), 'utf-8'); + const declared = Object.keys(packageJson.dependencies ?? {}); + + const imported = new Set(); + for (const match of contents.matchAll(IMPORT_PATTERN)) { + const specifier = match[1] ?? match[2]; + if (specifier && isBareSpecifier(specifier)) { + imported.add(toPackageName(specifier)); + } + } + + expect(declared).toEqual(expect.arrayContaining([...imported])); + }); +}); diff --git a/src/memory/package.json b/src/memory/package.json index e32d25a3e2..1a57ccc5dc 100644 --- a/src/memory/package.json +++ b/src/memory/package.json @@ -25,7 +25,8 @@ "test": "vitest run --coverage" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.29.0" + "@modelcontextprotocol/sdk": "^1.29.0", + "zod": "^4.0.0" }, "devDependencies": { "@types/node": "^22", diff --git a/src/sequentialthinking/__tests__/dependencies.test.ts b/src/sequentialthinking/__tests__/dependencies.test.ts new file mode 100644 index 0000000000..5624195872 --- /dev/null +++ b/src/sequentialthinking/__tests__/dependencies.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect } from 'vitest'; +import { readFileSync, readdirSync } from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { builtinModules } from 'module'; + +/** + * Guards against phantom dependencies: modules that are imported by the shipped + * source but not declared in package.json. Such imports resolve by accident + * under npm's hoisted node_modules layout, then fail at startup for anyone + * using a strict layout (pnpm, yarn PnP) with ERR_MODULE_NOT_FOUND. + */ +describe('declared dependencies', () => { + const packageDir = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); + + const packageJson = JSON.parse( + readFileSync(path.join(packageDir, 'package.json'), 'utf-8') + ) as { dependencies?: Record }; + + // Source files that get compiled into dist/, i.e. everything except tests and configs. + const sourceFiles = readdirSync(packageDir) + .filter((file) => file.endsWith('.ts')) + .filter((file) => !file.endsWith('.test.ts') && file !== 'vitest.config.ts'); + + const IMPORT_PATTERN = /(?:import|export)[\s\S]*?from\s*['"]([^'"]+)['"]|import\s*\(\s*['"]([^'"]+)['"]\s*\)/g; + + /** Reduces a specifier like `zod/v4` or `@scope/pkg/sub` to its package name. */ + const toPackageName = (specifier: string): string => { + const segments = specifier.split('/'); + return specifier.startsWith('@') ? segments.slice(0, 2).join('/') : segments[0]; + }; + + const isBareSpecifier = (specifier: string): boolean => + !specifier.startsWith('.') && + !specifier.startsWith('node:') && + !builtinModules.includes(toPackageName(specifier)); + + it('finds source files to check', () => { + expect(sourceFiles.length).toBeGreaterThan(0); + }); + + it.each(sourceFiles)('%s imports only declared packages', (file) => { + const contents = readFileSync(path.join(packageDir, file), 'utf-8'); + const declared = Object.keys(packageJson.dependencies ?? {}); + + const imported = new Set(); + for (const match of contents.matchAll(IMPORT_PATTERN)) { + const specifier = match[1] ?? match[2]; + if (specifier && isBareSpecifier(specifier)) { + imported.add(toPackageName(specifier)); + } + } + + expect(declared).toEqual(expect.arrayContaining([...imported])); + }); +}); diff --git a/src/sequentialthinking/package.json b/src/sequentialthinking/package.json index a88de803b5..e6a81dafbc 100644 --- a/src/sequentialthinking/package.json +++ b/src/sequentialthinking/package.json @@ -27,7 +27,8 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", "chalk": "^5.3.0", - "yargs": "^17.7.2" + "yargs": "^17.7.2", + "zod": "^4.0.0" }, "devDependencies": { "@types/node": "^22",