Skip to main content
Glama

Readwise MCP Server

by IAmAlexander
test-inspector.ts14.5 kB
#!/usr/bin/env node import { spawn, type ChildProcess } from 'child_process'; import { createInterface } from 'readline'; import { setTimeout } from 'timers/promises'; import chalk from 'chalk'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; import { execSync } from 'child_process'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); interface TestCase { name: string; command: string; expectedOutput?: string | RegExp; validate?: (output: string) => boolean; } class InspectorTestRunner { private serverProcess?: ChildProcess; private inspectorProcess?: ChildProcess; private testCases: TestCase[]; private results: { passed: string[]; failed: string[] } = { passed: [], failed: [] }; constructor() { // Define test cases this.testCases = [ { name: 'List Tools', command: 'list tools', validate: (output) => { const tools = [ 'get_highlights', 'get_books', 'get_documents', 'search_highlights' ]; return tools.every(tool => output.includes(tool)); } }, { name: 'List Prompts', command: 'list prompts', validate: (output) => { const prompts = [ 'readwise_highlight', 'readwise_search' ]; return prompts.every(prompt => output.includes(prompt)); } }, { name: 'Get Books', command: 'tool get_books --parameters {"page":1,"page_size":5}', validate: (output) => { try { const response = JSON.parse(output); return Array.isArray(response.books) && response.books.length <= 5; } catch { return false; } } }, { name: 'Get Highlights', command: 'tool get_highlights --parameters {"page":1,"page_size":5}', validate: (output) => { try { const response = JSON.parse(output); return Array.isArray(response.highlights) && response.highlights.length <= 5; } catch { return false; } } }, { name: 'Search Highlights', command: 'tool search_highlights --parameters {"query":"test"}', validate: (output) => { try { const response = JSON.parse(output); return Array.isArray(response.results); } catch { return false; } } }, { name: 'Get Documents', command: 'tool get_documents --parameters {"page":1,"page_size":5}', validate: (output) => { try { const response = JSON.parse(output); return Array.isArray(response.documents) && response.documents.length <= 5; } catch { return false; } } }, { name: 'Readwise Highlight Prompt', command: 'prompt readwise_highlight --parameters {"highlight_id":1}', validate: (output) => { return output.includes('highlight') || output.includes('error'); } }, { name: 'Readwise Search Prompt', command: 'prompt readwise_search --parameters {"query":"test"}', validate: (output) => { return output.includes('results') || output.includes('error'); } } ]; } private async startServer(): Promise<void> { return new Promise((resolve, reject) => { console.log('Starting server process...'); const serverPath = join(__dirname, '..', 'dist', 'index.js'); this.serverProcess = spawn('node', [serverPath], { env: { ...process.env, TRANSPORT: 'stdio' } }); let serverStarted = false; let serverError = ''; // Wait for server to be ready const checkStartup = (data: Buffer) => { const message = data.toString(); console.log('Server output:', message.trim()); if (message.includes('Server started on port')) { serverStarted = true; resolve(); } }; this.serverProcess.stdout?.on('data', checkStartup); this.serverProcess.stderr?.on('data', (data) => { const message = data.toString(); console.error(chalk.red(`Server error: ${message.trim()}`)); serverError += message; checkStartup(data); }); this.serverProcess.on('error', (error) => { console.error(chalk.red('Server process error:', error)); reject(error); }); this.serverProcess.on('exit', (code) => { if (!serverStarted) { console.error(chalk.red(`Server process exited with code ${code}`)); reject(new Error(`Server failed to start: ${serverError}`)); } }); // Set a timeout setTimeout(10000).then(() => { if (!serverStarted) { reject(new Error('Server startup timeout')); } }); }); } private async startInspector(): Promise<void> { return new Promise((resolve, reject) => { console.log('Starting inspector with server...'); // Kill any existing inspector processes and clear ports try { // Kill any existing inspector processes execSync('pkill -f "@modelcontextprotocol/inspector"'); console.log('Killed existing inspector processes'); // Kill any processes using port 3000 (inspector proxy) and 3002 (our server) execSync('lsof -ti:3000,3002 | xargs kill -9'); console.log('Killed processes using ports 3000 and 3002'); } catch (error) { // Ignore errors if no processes were found } // Wait a moment for ports to be released setTimeout(2000).then(() => { // Start the inspector with our server using the documented pattern const serverPath = join(__dirname, '..', 'dist', 'index.js'); // Basic environment for debugging const env = { ...process.env, DEBUG: '*', NODE_ENV: 'development' }; // Follow the documented pattern for passing environment variables with -e flag this.inspectorProcess = spawn('npx', [ '@modelcontextprotocol/inspector', '-e', 'MCP_API_KEY=pvRTHpiGinEMs1iKc1M6ylpoDnIDxyWSmU443Q7wyjY10ovdqG', '-e', 'MCP_TRANSPORT=stdio', '-e', 'MCP_PORT=3002', '--', 'node', serverPath, '--debug' ], { stdio: 'pipe', env, shell: true }); let inspectorStarted = false; let serverStarted = false; let errorOutput = ''; const checkStartup = (data: Buffer) => { const message = data.toString(); console.log('Output:', message.trim()); if (message.includes('MCP Inspector is up and running')) { console.log('✓ Inspector started'); inspectorStarted = true; } if (message.includes('spawn-rx spawning process')) { console.log('✓ Server process spawned'); // Server process is starting } if (message.includes('Starting Readwise MCP server')) { console.log('✓ Server starting'); // Don't set serverStarted yet, wait for tools to be registered } if (message.includes('Registered') && message.includes('tools')) { console.log('✓ Server initialized'); serverStarted = true; } // Only resolve when both inspector and server are ready if (inspectorStarted && serverStarted) { console.log('Both inspector and server are ready'); // Give it a moment to fully initialize global.setTimeout(() => resolve(), 1000); } }; // Listen to both stdout and stderr this.inspectorProcess.stdout?.on('data', checkStartup); this.inspectorProcess.stderr?.on('data', (data) => { const message = data.toString(); // Check for startup messages in stderr too checkStartup(data); if (!message.includes('ExperimentalWarning')) { // Ignore ts-node warnings console.error(chalk.red(`Error: ${message.trim()}`)); errorOutput += message; } }); this.inspectorProcess.on('error', (error) => { console.error(chalk.red('Process error:', error)); reject(error); }); this.inspectorProcess.on('exit', (code) => { if (!inspectorStarted || !serverStarted) { console.error(chalk.red(`Process exited with code ${code}`)); reject(new Error(`Failed to start: ${errorOutput}`)); } }); // Set a longer timeout since we're waiting for both to initialize setTimeout(30000).then(() => { if (!inspectorStarted || !serverStarted) { const status = []; if (!inspectorStarted) status.push('Inspector not started'); if (!serverStarted) status.push('Server not started'); reject(new Error(`Startup timeout: ${status.join(', ')}`)); } }); }); }); } private async runCommand(command: string): Promise<string> { return new Promise((resolve, reject) => { if (!this.inspectorProcess?.stdin || !this.inspectorProcess?.stdout) { reject(new Error('Inspector not running')); return; } console.log(`\nExecuting command: ${command}`); let output = ''; let commandSent = false; const outputHandler = (data: Buffer) => { const message = data.toString(); output += message; console.log('Command output:', message.trim()); // Look for the prompt character that indicates command completion if (message.includes('\n> ')) { this.inspectorProcess?.stdout?.removeListener('data', outputHandler); console.log('Command completed'); resolve(output.trim()); } }; this.inspectorProcess.stdout.on('data', outputHandler); // Send the command after a short delay to ensure the inspector is ready const sendCommand = () => { const timer = global.setTimeout(() => { try { console.log('Sending command to inspector...'); this.inspectorProcess?.stdin?.write(command + '\n'); commandSent = true; } catch (error) { reject(error); } }, 2000); return timer; }; // Set a longer timeout for command execution const commandTimeout = global.setTimeout(() => { this.inspectorProcess?.stdout?.removeListener('data', outputHandler); if (!commandSent) { reject(new Error('Failed to send command')); } else { reject(new Error('Command timeout')); } }, 30000); // Start the command sequence and handle any errors try { const sendTimer = sendCommand(); // Clean up on error this.inspectorProcess.on('error', () => { global.clearTimeout(sendTimer); global.clearTimeout(commandTimeout); }); } catch (error) { global.clearTimeout(commandTimeout); reject(error); } // Clean up on success this.inspectorProcess.stdout?.once('end', () => { global.clearTimeout(commandTimeout); }); }); } private async cleanup(): Promise<void> { console.log('Cleaning up processes...'); if (this.inspectorProcess) { this.inspectorProcess.kill(); console.log('Killed inspector process'); } // Kill any remaining inspector processes try { execSync('pkill -f "@modelcontextprotocol/inspector"'); console.log('Killed any remaining inspector processes'); } catch (error) { // Ignore errors if no processes were found } // Kill any processes using our ports try { // Kill processes using ports 3000 (inspector UI), 3001 (server default), and 3002 (our server) execSync('lsof -ti:3000,3001,3002 | xargs kill -9'); console.log('Killed processes using ports 3000, 3001, and 3002'); } catch (error) { // Ignore errors if no processes were found console.log('No processes found using ports 3000, 3001, or 3002'); } // Wait a moment for ports to be fully released await setTimeout(1000); } private logResult(testName: string, passed: boolean, output?: string): void { const status = passed ? chalk.green('✓ PASS') : chalk.red('✗ FAIL'); console.log(`${status} ${testName}`); if (!passed && output) { console.log(chalk.gray(output)); } if (passed) { this.results.passed.push(testName); } else { this.results.failed.push(testName); } } public async runTests(): Promise<boolean> { try { // Clean up any existing processes before starting await this.cleanup(); console.log(chalk.blue('Starting inspector with server...')); await this.startInspector(); console.log(chalk.blue('\nRunning tests...\n')); for (const test of this.testCases) { try { const output = await this.runCommand(test.command); const passed = test.validate ? test.validate(output) : test.expectedOutput ? (test.expectedOutput instanceof RegExp ? test.expectedOutput.test(output) : output.includes(test.expectedOutput) ) : true; this.logResult(test.name, passed, passed ? undefined : output); } catch (error) { this.logResult(test.name, false, String(error)); } } console.log('\nTest Summary:'); console.log(chalk.green(`Passed: ${this.results.passed.length}`)); console.log(chalk.red(`Failed: ${this.results.failed.length}`)); return this.results.failed.length === 0; } catch (error) { console.error(chalk.red('\nTest run failed:'), error); return false; } finally { await this.cleanup(); } } } // Run tests const runner = new InspectorTestRunner(); runner.runTests() .then(success => { process.exit(success ? 0 : 1); }) .catch(error => { console.error(chalk.red('Fatal error:'), error); process.exit(1); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/IAmAlexander/readwise-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server