Skip to main content
Glama
index.ts9.56 kB
#!/usr/bin/env node import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { z } from 'zod' import { ProcessManager } from './process-manager' import { Config } from './types' import { readFileSync, existsSync } from 'fs' import { resolve } from 'path' const server = new McpServer({ name: 'mcp-rewatch', version: '1.0.0' }) let processManager: ProcessManager function loadConfig(): Config { const configPath = resolve(process.cwd(), 'rewatch.config.json') // Check if config file exists if (!existsSync(configPath)) { console.error(`Configuration file not found at: ${configPath}`) console.error('Please create a rewatch.config.json file. See rewatch.config.example.json for reference.') process.exit(1) } try { const configContent = readFileSync(configPath, 'utf-8') const parsed = JSON.parse(configContent) // Validate config structure if (!parsed.processes || typeof parsed.processes !== 'object') { throw new Error('Invalid config: "processes" must be an object') } // Validate each process config for (const [name, processConfig] of Object.entries(parsed.processes)) { if (!processConfig || typeof processConfig !== 'object') { throw new Error(`Invalid process config for "${name}": must be an object`) } const config = processConfig as any if (!config.command || typeof config.command !== 'string') { throw new Error(`Invalid process config for "${name}": "command" is required and must be a string`) } } return parsed } catch (error) { if (error instanceof SyntaxError) { console.error('Invalid JSON in rewatch.config.json:', error.message) } else if ((error as any).code === 'EACCES') { console.error('Permission denied reading rewatch.config.json') } else { console.error('Failed to load config:', error instanceof Error ? error.message : error) } process.exit(1) } } // Load config and initialize process manager when server starts console.error('Loading configuration from:', resolve(process.cwd(), 'rewatch.config.json')) const config = loadConfig() console.error(`Loaded ${Object.keys(config.processes).length} process configuration(s):`, Object.keys(config.processes).join(', ')) processManager = new ProcessManager(config.processes) // Register tools server.registerTool( 'restart_process', { title: 'Restart Process', description: 'Stop and restart a development process', inputSchema: { name: z.string().describe('Process name (e.g., "frontend", "backend")') } }, async ({ name }) => { try { // Validate input if (!name || typeof name !== 'string' || name.trim() === '') { return { content: [{ type: 'text', text: 'Error: Process name must be a non-empty string' }] } } // Check if process exists const processes = processManager.listProcesses() const processNames = processes.map(p => p.name) if (!processNames.includes(name)) { return { content: [{ type: 'text', text: `Error: Process '${name}' not found. Available processes: ${processNames.join(', ') || 'none'}` }] } } const result = await processManager.restart(name) let message = `Process '${name}' ` if (result.success) { message += 'started successfully\n\n' message += 'Initial logs:\n' message += result.logs.length > 0 ? result.logs.join('\n') : 'No output yet' } else { message += 'failed to start or exited early\n\n' message += 'Logs:\n' message += result.logs.length > 0 ? result.logs.join('\n') : 'No output captured' } return { content: [ { type: 'text', text: message } ] } } catch (error) { return { content: [{ type: 'text', text: `Error restarting process '${name}': ${error instanceof Error ? error.message : String(error)}` }] } } } ) server.registerTool( 'get_process_logs', { title: 'Get Process Logs', description: 'Retrieve logs from a process', inputSchema: { name: z.string().describe('Process name'), lines: z.number().optional().describe('Number of recent lines to retrieve (default: all)') } }, async ({ name, lines }) => { try { // Validate name if (!name || typeof name !== 'string' || name.trim() === '') { return { content: [{ type: 'text', text: 'Error: Process name must be a non-empty string' }] } } // Validate lines parameter if provided if (lines !== undefined) { if (typeof lines !== 'number' || lines < 1 || lines > 10000 || !Number.isInteger(lines)) { return { content: [{ type: 'text', text: 'Error: lines must be an integer between 1 and 10000' }] } } } // Check if process exists const processes = processManager.listProcesses() const processNames = processes.map(p => p.name) if (!processNames.includes(name)) { return { content: [{ type: 'text', text: `Error: Process '${name}' not found. Available processes: ${processNames.join(', ') || 'none'}` }] } } const logs = processManager.getLogs(name, lines) return { content: [ { type: 'text', text: logs.length > 0 ? logs.join('\n') : 'No logs available' } ] } } catch (error) { return { content: [{ type: 'text', text: `Error retrieving logs: ${error instanceof Error ? error.message : String(error)}` }] } } } ) server.registerTool( 'stop_all', { title: 'Stop All Processes', description: 'Stop all running processes', inputSchema: {} }, async () => { try { await processManager.stopAll() return { content: [ { type: 'text', text: 'All processes stopped' } ] } } catch (error) { return { content: [{ type: 'text', text: `Error stopping processes: ${error instanceof Error ? error.message : String(error)}` }] } } } ) server.registerTool( 'list_processes', { title: 'List Processes', description: 'List all configured processes and their status', inputSchema: {} }, async () => { try { const processes = processManager.listProcesses() const output = processes.map(p => { let line = `${p.name}: ${p.status}` if (p.pid) line += ` (PID: ${p.pid})` if (p.error) line += ` - Error: ${p.error}` return line }).join('\n') return { content: [ { type: 'text', text: output || 'No processes configured' } ] } } catch (error) { return { content: [{ type: 'text', text: `Error listing processes: ${error instanceof Error ? error.message : String(error)}` }] } } } ) async function main() { try { const transport = new StdioServerTransport() await server.connect(transport) console.error('MCP Rewatch server started successfully') // Handle shutdown gracefully const shutdown = async (signal: string) => { console.error(`\\nReceived ${signal}, shutting down gracefully...`) try { await processManager.stopAll() console.error('All processes stopped') } catch (error) { console.error('Error during shutdown:', error instanceof Error ? error.message : error) } process.exit(0) } process.on('SIGTERM', () => shutdown('SIGTERM')) process.on('SIGINT', () => shutdown('SIGINT')) // Also handle unexpected exits process.on('exit', () => { // Synchronously try to kill all processes const processes = processManager.getProcesses() for (const [name, managed] of processes) { if (managed.process && managed.pid) { try { if (process.platform !== 'win32') { process.kill(-managed.pid, 'SIGKILL') } else { process.kill(managed.pid, 'SIGKILL') } } catch (e) { // Ignore errors during emergency cleanup } } } }) } catch (error) { if ((error as any).code === 'EPIPE') { console.error('Lost connection to MCP client') } else if (error instanceof Error && error.message.includes('transport')) { console.error('Failed to establish MCP transport:', error.message) } else { console.error('Fatal error during startup:', error instanceof Error ? error.message : error) } // Attempt cleanup try { await processManager?.stopAll() } catch (cleanupError) { console.error('Error during cleanup:', cleanupError instanceof Error ? cleanupError.message : cleanupError) } process.exit(1) } } main().catch((error) => { console.error('Unhandled error:', error) process.exit(1) })

Latest Blog Posts

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/brennancheung/mcp-rewatch'

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