Chrome Debug MCP Server
#!/usr/bin/env node
/**
* Chrome Debug MCP Server
*
* This server provides a Model Context Protocol (MCP) interface for controlling Chrome
* through the Chrome DevTools Protocol (CDP) and Puppeteer. It allows:
* - Launching Chrome with various configurations
* - Injecting userscripts with GM_ function support
* - Loading Chrome extensions
* - Capturing console logs
* - Evaluating JavaScript in the browser context
*
* @module ChromeDebugMCP
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ErrorCode,
McpError,
Request
} from '@modelcontextprotocol/sdk/types.js';
import CDP from 'chrome-remote-interface';
import type { Client } from 'chrome-remote-interface';
import * as puppeteer from 'puppeteer';
interface ConsoleAPICalledEvent {
type: string;
args: Array<{
value?: any;
description?: string;
}>;
}
import { readFile } from 'fs/promises';
import { join } from 'path';
// Enable verbose logging for debugging
const DEBUG = true;
const log = (...args: any[]) => DEBUG && console.log('[Chrome Debug MCP]', ...args);
/**
* Structure for console messages captured from Chrome
*/
interface ConsoleMessage {
/** The type of console message (log, warn, error, etc.) */
type: string;
/** The actual message content */
text: string;
}
/**
* Arguments for launching Chrome with specific configurations
*/
interface LaunchChromeArgs {
/** URL to navigate to after launch */
url?: string;
/** Path to a specific Chrome executable (uses bundled Chrome if not provided) */
executablePath?: string;
/** Path to a specific user data directory (optional, uses default Chrome profile if not provided) */
userDataDir?: string;
/** Path to an unpacked Chrome extension to load */
loadExtension?: string;
/** Path to extension that should remain enabled while others are disabled */
disableExtensionsExcept?: string;
/** Whether to disable Chrome's "Automation Controlled" banner */
disableAutomationControlled?: boolean;
/** Path to a userscript file to inject into the page */
userscriptPath?: string;
}
/**
* Arguments for retrieving console logs
*/
interface GetConsoleLogsArgs {
/** Whether to clear the logs after retrieving them */
clear?: boolean;
}
/**
* Arguments for evaluating JavaScript in Chrome
*/
interface EvaluateArgs {
/** JavaScript code to evaluate in the browser context */
expression?: string;
}
/**
* Main server class that handles Chrome debugging and MCP communication
*/
class ChromeDebugServer {
private server: Server;
private browser: puppeteer.Browser | null = null;
private cdpClient: Client | null = null;
private consoleLogs: string[] = [];
constructor() {
// Initialize MCP server with basic configuration
this.server = new Server(
{
name: 'chrome-debug-mcp',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
this.setupToolHandlers();
this.server.onerror = (error) => console.error('[MCP Error]', error);
}
/**
* Sets up handlers for all supported MCP tools.
* This includes tool listing and execution of individual tools.
*
* @private
*/
private setupToolHandlers() {
// Handler for listing available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'launch_chrome',
description: 'Launch Chrome in debug mode',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'URL to navigate to (optional)',
},
executablePath: {
type: 'string',
description: 'Path to Chrome executable (optional, uses bundled Chrome if not provided)',
},
userDataDir: {
type: 'string',
description: 'Path to a specific user data directory (optional, uses default Chrome profile if not provided)',
},
loadExtension: {
type: 'string',
description: 'Path to unpacked extension directory to load (optional)',
},
disableExtensionsExcept: {
type: 'string',
description: 'Path to extension that should remain enabled while others are disabled (optional)',
},
disableAutomationControlled: {
type: 'boolean',
description: 'Disable Chrome\'s "Automation Controlled" mode (optional, default: false)',
},
userscriptPath: {
type: 'string',
description: 'Path to userscript file to inject (optional)',
},
},
},
},
{
name: 'get_console_logs',
description: 'Get console logs from Chrome',
inputSchema: {
type: 'object',
properties: {
clear: {
type: 'boolean',
description: 'Whether to clear logs after retrieving',
},
},
},
},
{
name: 'evaluate',
description: 'Evaluate JavaScript in Chrome',
inputSchema: {
type: 'object',
properties: {
expression: {
type: 'string',
description: 'JavaScript code to evaluate',
},
},
required: ['expression'],
},
},
],
}));
// Handler for executing tools
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const args = request.params.arguments || {};
switch (request.params.name) {
case 'launch_chrome':
return this.handleLaunchChrome(args as LaunchChromeArgs);
case 'get_console_logs':
return this.handleGetConsoleLogs(args as GetConsoleLogsArgs);
case 'evaluate':
return this.handleEvaluate(args as EvaluateArgs);
default:
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${request.params.name}`
);
}
});
}
/**
* Handles the launch_chrome tool request.
* This launches Chrome with specified configurations and sets up CDP connection.
*
* @param args - Configuration options for launching Chrome
* @returns MCP response with launch status
* @throws McpError if launch fails
* @private
*/
private async handleLaunchChrome(args: LaunchChromeArgs): Promise<any> {
try {
// Close existing browser if any
if (this.browser) {
await this.browser.close();
}
// Configure Chrome launch options
const launchOptions: puppeteer.PuppeteerLaunchOptions = {
headless: false,
ignoreDefaultArgs: ['--disable-extensions'], // Prevent Puppeteer from disabling extensions
args: [
'--remote-debugging-port=9222', // Enable CDP
'--disable-web-security', // Allow cross-origin requests
'--no-sandbox' // Required for some environments
]
} as puppeteer.PuppeteerLaunchOptions;
// Configure user data directory if specified
if (args?.userDataDir) {
log('Using custom user data directory:', args.userDataDir);
launchOptions.userDataDir = args.userDataDir;
} else {
log('Using default Chrome profile');
}
// Configure extension loading if requested
if (args?.loadExtension) {
log('Loading extension from:', args.loadExtension);
launchOptions.args?.push(`--load-extension=${args.loadExtension}`);
}
if (args?.disableExtensionsExcept) {
log('Disabling extensions except:', args.disableExtensionsExcept);
launchOptions.args?.push(`--disable-extensions-except=${args.disableExtensionsExcept}`);
}
// Use specific Chrome executable if provided
if (args?.executablePath) {
log('Using custom Chrome executable:', args.executablePath);
launchOptions.executablePath = args.executablePath;
} else {
log('Using bundled Chrome executable');
}
// Handle automation mode configuration
if (args?.disableAutomationControlled) {
log('Disabling automation controlled mode');
if (!launchOptions.ignoreDefaultArgs) {
launchOptions.ignoreDefaultArgs = [];
}
if (Array.isArray(launchOptions.ignoreDefaultArgs)) {
launchOptions.ignoreDefaultArgs.push('--enable-automation');
}
}
// Launch Chrome using Puppeteer
this.browser = await puppeteer.launch(launchOptions);
const pages = await this.browser.pages();
const page = pages[0];
// Set up CDP client for advanced debugging capabilities
this.cdpClient = await CDP();
// Enable console monitoring
await this.cdpClient.Console.enable();
await this.cdpClient.Runtime.enable();
// Set up console message capture
this.cdpClient.Runtime.consoleAPICalled((params: ConsoleAPICalledEvent) => {
const { type, args } = params;
const text = args.map((arg: { value?: any; description?: string }) => arg.value || arg.description).join(' ');
this.consoleLogs.push(`[${type}] ${text}`);
log('Console message:', type, text);
});
if (args?.url) {
// Navigate to specified URL
log('Navigating to target URL...');
await page.goto(args.url, { waitUntil: 'networkidle0' });
// Handle userscript injection if requested
let scriptContent = '';
if (args?.userscriptPath) {
try {
log('Reading userscript from:', args.userscriptPath);
scriptContent = await readFile(args.userscriptPath, 'utf8');
} catch (error) {
log('Error reading userscript:', error);
throw new McpError(
ErrorCode.InternalError,
`Failed to read userscript: ${error}`
);
}
}
// Inject Greasemonkey-style functions and userscript
log('Injecting GM functions' + (scriptContent ? ' and userscript' : ''));
await page.evaluate((content: string) => {
try {
// Define GM_ functions that userscripts can use
const gmFunctionsScript = `
// Store values persistently
window.GM_setValue = function(key, value) {
localStorage.setItem('GM_' + key, JSON.stringify(value));
};
// Retrieve stored values
window.GM_getValue = function(key, defaultValue) {
const value = localStorage.getItem('GM_' + key);
return value ? JSON.parse(value) : defaultValue;
};
// Make HTTP requests (simplified implementation)
window.GM_xmlhttpRequest = function(details) {
fetch(details.url)
.then(r => r.text())
.then(text => details.onload?.({ responseText: text }))
.catch(err => details.onerror?.(err));
};
// Add CSS to the page
window.GM_addStyle = function(css) {
const style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
};
// Open URL in new tab
window.GM_openInTab = function(url) {
window.open(url, '_blank');
};
// Register menu commands (stub implementation)
window.GM_registerMenuCommand = function(name, fn) {
// Stub for menu command registration
};
// Initialize API key if needed
if (!localStorage.getItem('GM_nzbgeekApiKey')) {
localStorage.setItem('GM_nzbgeekApiKey', JSON.stringify('CuJU1bkXcsvYmuXjpK9HtyjTimWw8Zm0'));
}
`;
// Inject GM functions
const gmScript = document.createElement('script');
gmScript.textContent = gmFunctionsScript;
document.head.appendChild(gmScript);
// Inject userscript if provided
if (content) {
const userScript = document.createElement('script');
userScript.textContent = content;
document.head.appendChild(userScript);
console.log('[Chrome Debug MCP] GM functions and userscript injected successfully');
} else {
console.log('[Chrome Debug MCP] GM functions injected successfully (no userscript provided)');
}
} catch (error) {
console.error('[Chrome Debug MCP] Failed to inject userscript:', error);
}
}, scriptContent);
}
// Get Chrome version info
const version = await this.browser.version();
// Build detailed status message
let statusMessage = `Chrome launched successfully in debug mode\n${version}`;
// Add configuration details to status
statusMessage += args?.executablePath
? `\nUsing custom executable: ${args.executablePath}`
: '\nUsing bundled Chrome';
statusMessage += args?.userDataDir
? `\nUsing custom user data directory: ${args.userDataDir}`
: '\nUsing default Chrome profile';
if (args?.loadExtension) {
statusMessage += `\nLoaded extension: ${args.loadExtension}`;
}
if (args?.disableExtensionsExcept) {
statusMessage += `\nDisabled extensions except: ${args.disableExtensionsExcept}`;
}
if (args?.disableAutomationControlled) {
statusMessage += '\nAutomation controlled mode disabled';
}
if (args?.userscriptPath) {
statusMessage += `\nInjected userscript: ${args.userscriptPath}`;
}
return {
content: [
{
type: 'text',
text: statusMessage,
},
],
};
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Failed to launch Chrome: ${error}`
);
}
}
/**
* Handles the get_console_logs tool request.
* Returns captured console messages and optionally clears the log.
*
* @param args - Configuration for log retrieval
* @returns MCP response with console logs
* @throws McpError if Chrome is not running
* @private
*/
private async handleGetConsoleLogs(args: GetConsoleLogsArgs) {
if (!this.browser) {
throw new McpError(
ErrorCode.InternalError,
'Chrome is not running. Call launch_chrome first.'
);
}
const logs = [...this.consoleLogs];
if (args?.clear) {
this.consoleLogs = [];
}
return {
content: [
{
type: 'text',
text: logs.join('\n') || 'No console logs available',
},
],
};
}
/**
* Handles the evaluate tool request.
* Executes JavaScript code in the browser context and returns the result.
*
* @param args - JavaScript code to evaluate
* @returns MCP response with evaluation result
* @throws McpError if Chrome is not running or evaluation fails
* @private
*/
private async handleEvaluate(args: EvaluateArgs) {
if (!this.browser || !this.cdpClient) {
throw new McpError(
ErrorCode.InternalError,
'Chrome is not running. Call launch_chrome first.'
);
}
if (!args?.expression) {
throw new McpError(
ErrorCode.InvalidParams,
'Expression is required'
);
}
try {
const result = await this.cdpClient.Runtime.evaluate({
expression: args.expression,
returnByValue: true,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(result.result, null, 2),
},
],
};
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Evaluation failed: ${error}`
);
}
}
/**
* Starts the MCP server using stdio transport.
* This allows the server to communicate with the MCP client through standard input/output.
*/
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Chrome Debug MCP server running on stdio');
}
/**
* Performs cleanup when shutting down the server.
* Closes Chrome and CDP connections gracefully.
*/
async cleanup() {
if (this.cdpClient) {
await this.cdpClient.close();
}
if (this.browser) {
await this.browser.close();
}
await this.server.close();
}
}
// Create and start server instance
const server = new ChromeDebugServer();
// Handle graceful shutdown
process.on('SIGINT', async () => {
await server.cleanup();
process.exit(0);
});
// Start the server
server.run().catch(console.error);