mcp-rewatch
Version:
MCP server that enables Claude Code to manage long-running development processes
304 lines • 10.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
const zod_1 = require("zod");
const process_manager_1 = require("./process-manager");
const fs_1 = require("fs");
const path_1 = require("path");
const server = new mcp_js_1.McpServer({
name: 'mcp-rewatch',
version: '1.0.0'
});
let processManager;
function loadConfig() {
const configPath = (0, path_1.resolve)(process.cwd(), 'rewatch.config.json');
// Check if config file exists
if (!(0, fs_1.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 = (0, fs_1.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;
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.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:', (0, path_1.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 process_manager_1.ProcessManager(config.processes);
// Register tools
server.registerTool('restart_process', {
title: 'Restart Process',
description: 'Stop and restart a development process',
inputSchema: {
name: zod_1.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: zod_1.z.string().describe('Process name'),
lines: zod_1.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 stdio_js_1.StdioServerTransport();
await server.connect(transport);
console.error('MCP Rewatch server started successfully');
// Handle shutdown gracefully
const shutdown = async (signal) => {
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.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);
});
//# sourceMappingURL=index.js.map