Files
obra__episodic-memory/cli/mcp-server-wrapper.js
Jesse Vincent 32c8382c2d fix: probe each required package's manifest before launching the MCP server (#95 Bug 1)
cli/mcp-server-wrapper.js previously checked only `existsSync(node_modules)`
to decide whether to run `npm install`. A partial extraction — the
directory exists, but a package is missing its package.json and lib/ —
slips past that check, hands off to dist/mcp-server.js, and crashes with
a confusing `ERR_MODULE_NOT_FOUND` after the wrapper has already declared
deps healthy. The reporter on Windows 11 saw exactly this for
better-sqlite3 (folder contained only `deps/` and `LICENSE`).

New module cli/install-check.js exports findMissingDeps(pluginRoot) which
returns the list of runtime-required packages whose package.json is
missing under node_modules. The wrapper now logs the missing packages
before re-running `npm install`, giving the user a useful diagnostic when
the partial-extract case strikes again.

Probing each package's manifest — not just the directory — catches the
specific failure shape that originally motivated the issue. Optional and
OS-specific externals (sharp, fsevents) are deliberately excluded from
the check.

Tests in test/install-check.test.ts cover the no-node_modules case, the
empty-node_modules case, the all-present happy path, the
partial-extraction case (better-sqlite3 dir present but manifest gone),
multi-missing reporting, and the optional-deps-excluded behavior.

Addresses Bug 1 of #95; Bug 2 (onnxruntime-common hoisting) is deferred
pending a Windows reproduction.
2026-05-21 10:22:50 -07:00

125 lines
4.0 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* Cross-platform wrapper script for MCP server that ensures dependencies are installed
* This runs before the MCP server starts and works on Windows, macOS, and Linux
*/
import { spawn } from 'child_process';
import { existsSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import { findMissingDeps } from './install-check.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Determine plugin root directory
const PLUGIN_ROOT = process.env.CLAUDE_PLUGIN_ROOT || join(__dirname, '..');
// Helper function to run npm install
function runNpmInstall() {
return new Promise((resolve, reject) => {
const isWindows = process.platform === 'win32';
const npmCommand = isWindows ? 'npm.cmd' : 'npm';
console.error('Installing episodic-memory dependencies (first run only)...');
console.error('This may take 30-60 seconds...');
// Install dependencies - npm will auto-install optionalDependencies for current platform
const child = spawn(npmCommand, ['install', '--no-audit', '--no-fund'], {
cwd: PLUGIN_ROOT,
stdio: ['ignore', 'pipe', 'pipe'],
shell: isWindows // On Windows, we need shell: true to find npm.cmd
});
child.stdout.on('data', (data) => {
// Suppress npm install output to stderr to avoid cluttering MCP logs
process.stderr.write(data);
});
child.stderr.on('data', (data) => {
process.stderr.write(data);
});
child.on('exit', (code) => {
if (code === 0) {
console.error('Dependencies installed successfully.');
resolve();
} else {
console.error('ERROR: Failed to install dependencies.');
console.error(`Please run manually: cd "${PLUGIN_ROOT}" && npm install`);
reject(new Error(`npm install failed with exit code ${code}`));
}
});
child.on('error', (err) => {
console.error(`ERROR: Failed to run npm install: ${err.message}`);
reject(err);
});
});
}
async function main() {
try {
// Probe each required runtime dependency's package.json — not just the
// node_modules directory. A partial extraction (folder exists but the
// package is missing its manifest and lib/) would slip past an existsSync
// check on node_modules alone and crash the server with ERR_MODULE_NOT_FOUND
// *after* the wrapper has handed off to dist/mcp-server.js (#95 Bug 1).
const missing = findMissingDeps(PLUGIN_ROOT);
if (missing.length > 0) {
console.error(`Missing dependencies under node_modules: ${missing.join(', ')}`);
await runNpmInstall();
}
// Start the MCP server
const mcpServerPath = join(PLUGIN_ROOT, 'dist', 'mcp-server.js');
if (!existsSync(mcpServerPath)) {
console.error(`ERROR: MCP server not found at ${mcpServerPath}`);
console.error('Please run: npm run build');
process.exit(1);
}
// Use spawn with shell: false for better cross-platform compatibility
const child = spawn(process.execPath, [mcpServerPath], {
stdio: 'inherit',
shell: false
});
// Forward signals to the child process
process.on('SIGTERM', () => child.kill('SIGTERM'));
process.on('SIGINT', () => child.kill('SIGINT'));
process.on('SIGHUP', () => child.kill('SIGHUP'));
// Detect parent process death via stdin close
// When Claude exits (normally or abnormally), stdin will close
process.stdin.on('end', () => {
child.kill();
process.exit(0);
});
child.on('exit', (code, signal) => {
if (signal) {
process.kill(process.pid, signal);
} else {
process.exit(code || 0);
}
});
child.on('error', (err) => {
console.error(`ERROR: Failed to start MCP server: ${err.message}`);
process.exit(1);
});
} catch (error) {
console.error(`ERROR: ${error.message}`);
process.exit(1);
}
}
main().catch((error) => {
console.error(`Unexpected error: ${error.message}`);
process.exit(1);
});