UNPKG

@thestarware/chrome-console-mcp

Version:

MCP Server for Chrome/Edge console logs and network requests via browser extension

439 lines 18 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListResourcesRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { ConsoleLogManager } from './console-manager.js'; import { HttpLogBridge } from './http-bridge.js'; class ChromeConsoleMCPServer { server; consoleManager; httpBridge; constructor(port = 8765) { this.server = new Server({ name: 'chrome-console-mcp', version: '1.0.0', }, { capabilities: { resources: {}, tools: {}, }, }); this.consoleManager = new ConsoleLogManager(); this.httpBridge = new HttpLogBridge(this.consoleManager, port); this.setupHandlers(); } setupHandlers() { // Resource handlers this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [ { uri: 'console://logs/all', mimeType: 'application/json', name: 'All Logs', description: 'All captured browser console logs and network requests', }, { uri: 'console://logs/recent', mimeType: 'application/json', name: 'Recent Logs', description: 'Last 100 logs (console + network)', }, { uri: 'console://logs/console', mimeType: 'application/json', name: 'Console Logs Only', description: 'Only browser console logs from Chrome/Edge', }, { uri: 'console://logs/network', mimeType: 'application/json', name: 'Network Requests Only', description: 'Only network requests from Chrome/Edge', }, { uri: 'console://stats', mimeType: 'application/json', name: 'Log Statistics', description: 'Statistics about captured browser logs', }, ], })); this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const { uri } = request.params; switch (uri) { case 'console://logs/all': return { contents: [ { uri, mimeType: 'application/json', text: JSON.stringify(this.consoleManager.getLogs(), null, 2), }, ], }; case 'console://logs/recent': return { contents: [ { uri, mimeType: 'application/json', text: JSON.stringify(this.consoleManager.getLogs(undefined, 100), null, 2), }, ], }; case 'console://logs/console': return { contents: [ { uri, mimeType: 'application/json', text: JSON.stringify(this.consoleManager.getLogs({ type: ['console-log'] }), null, 2), }, ], }; case 'console://logs/network': return { contents: [ { uri, mimeType: 'application/json', text: JSON.stringify(this.consoleManager.getLogs({ type: ['network-request'] }), null, 2), }, ], }; case 'console://stats': this.consoleManager.getLogCount(); // This will log the current count return { contents: [ { uri, mimeType: 'application/json', text: JSON.stringify(this.consoleManager.getLogStats(), null, 2), }, ], }; default: throw new McpError(ErrorCode.InvalidRequest, `Unknown resource: ${uri}`); } }); // Tool handlers this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'search_logs', description: 'Search browser console logs and network requests by various criteria', inputSchema: { type: 'object', properties: { searchTerm: { type: 'string', description: 'Search term to filter logs', }, type: { type: 'array', items: { type: 'string', enum: ['console-log', 'network-request'], }, description: 'Types of logs to include', }, level: { type: 'array', items: { type: 'string', enum: ['log', 'warn', 'error', 'info', 'debug'], }, description: 'Console log levels to include', }, method: { type: 'array', items: { type: 'string', }, description: 'HTTP methods to include for network requests', }, status: { type: 'array', items: { type: 'number', }, description: 'HTTP status codes to include for network requests', }, url: { type: 'string', description: 'URL pattern to filter by', }, limit: { type: 'number', description: 'Maximum number of logs to return', default: 100, }, }, }, }, { name: 'export_logs', description: 'Export browser console logs and network requests in various formats', inputSchema: { type: 'object', properties: { format: { type: 'string', enum: ['json', 'csv', 'txt'], description: 'Export format', default: 'json', }, filter: { type: 'object', properties: { type: { type: 'array', items: { type: 'string', enum: ['console-log', 'network-request'], }, }, level: { type: 'array', items: { type: 'string', enum: ['log', 'warn', 'error', 'info', 'debug'], }, }, method: { type: 'array', items: { type: 'string', }, }, status: { type: 'array', items: { type: 'number', }, }, url: { type: 'string', }, searchTerm: { type: 'string', }, }, }, limit: { type: 'number', description: 'Maximum number of logs to export', }, }, required: ['format'], }, }, { name: 'clear_logs', description: 'Clear all stored browser console logs and network requests', inputSchema: { type: 'object', properties: {}, }, }, { name: 'get_log_stats', description: 'Get statistics about browser console logs', inputSchema: { type: 'object', properties: {}, }, }, { name: 'get_server_info', description: 'Get server information including current port', inputSchema: { type: 'object', properties: {}, }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; switch (name) { case 'search_logs': { const filter = {}; if (args?.type) filter.type = args.type; if (args?.level) filter.level = args.level; if (args?.method) filter.method = args.method; if (args?.status) filter.status = args.status; if (args?.url) filter.url = args.url; if (args?.searchTerm) filter.searchTerm = args.searchTerm; const logs = this.consoleManager.getLogs(filter, args?.limit); return { content: [ { type: 'text', text: `Found ${logs.length} matching logs:\n\n${JSON.stringify(logs, null, 2)}`, }, ], }; } case 'export_logs': { const options = { format: args?.format || 'json', filter: args?.filter, limit: args?.limit, }; const exportedData = this.consoleManager.exportLogs(options); return { content: [ { type: 'text', text: `Logs exported in ${options.format} format:\n\n${exportedData}`, }, ], }; } case 'clear_logs': { this.consoleManager.clearLogs(); return { content: [ { type: 'text', text: 'All logs have been cleared.', }, ], }; } case 'get_log_stats': { const stats = this.consoleManager.getLogStats(); return { content: [ { type: 'text', text: `Log Statistics:\n${JSON.stringify(stats, null, 2)}`, }, ], }; } case 'get_server_info': { const serverInfo = { port: this.httpBridge.getPort(), status: 'running', version: '1.0.0' }; return { content: [ { type: 'text', text: `Server Information:\n${JSON.stringify(serverInfo, null, 2)}`, }, ], }; } default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } }); } async run() { // Start HTTP bridge this.httpBridge.start(); const transport = new StdioServerTransport(); await this.server.connect(transport); } cleanup() { console.error('Shutting down MCP server...'); this.httpBridge.stop(); console.error('HTTP bridge stopped.'); } } // Parse command line arguments const args = process.argv.slice(2); let port = 8765; for (let i = 0; i < args.length; i++) { if (args[i] === '--port' && args[i + 1]) { const parsedPort = parseInt(args[i + 1], 10); if (!isNaN(parsedPort) && parsedPort > 0 && parsedPort <= 65535) { port = parsedPort; } else { console.error('Invalid port number. Using default port 8765.'); } break; } } // Start the server const server = new ChromeConsoleMCPServer(port); // Setup cleanup handlers for graceful shutdown function setupCleanupHandlers() { let isShuttingDown = false; const cleanup = (reason) => { if (isShuttingDown) return; isShuttingDown = true; if (reason) { console.error(`Shutdown triggered by: ${reason}`); } server.cleanup(); process.exit(0); }; // Handle normal process termination process.on('SIGINT', () => cleanup('SIGINT')); // Ctrl+C process.on('SIGTERM', () => cleanup('SIGTERM')); // Termination signal process.on('SIGHUP', () => cleanup('SIGHUP')); // Hangup signal // Handle uncaught exceptions process.on('uncaughtException', (error) => { console.error('Uncaught exception:', error); if (!isShuttingDown) { isShuttingDown = true; server.cleanup(); process.exit(1); } }); // Handle unhandled promise rejections process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled rejection at:', promise, 'reason:', reason); if (!isShuttingDown) { isShuttingDown = true; server.cleanup(); process.exit(1); } }); // Handle when parent process (Claude) disconnects process.on('disconnect', () => cleanup('parent disconnect')); // Handle stdin close (when Claude closes the pipe) process.stdin.on('close', () => cleanup('stdin close')); process.stdin.on('end', () => cleanup('stdin end')); // Handle when parent process dies (check periodically) const checkParent = () => { if (isShuttingDown) return; try { // If parent process is dead, this will throw process.kill(process.ppid, 0); } catch (error) { cleanup('parent process died'); } }; // Check parent every 5 seconds const parentCheckInterval = setInterval(checkParent, 5000); // Store interval to clear it on any exit const cleanupInterval = () => { clearInterval(parentCheckInterval); }; // Ensure interval is cleared when process exits process.once('exit', cleanupInterval); process.once('beforeExit', cleanupInterval); } setupCleanupHandlers(); server.run().catch((error) => { console.error('Failed to start MCP server:', error); server.cleanup(); process.exit(1); }); //# sourceMappingURL=index.js.map