@thestarware/chrome-console-mcp
Version:
MCP Server for Chrome/Edge console logs and network requests via browser extension
439 lines • 18 kB
JavaScript
#!/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