browser-connect-mcp
Version:
MCP server for browser DevTools and backend debugging - analyze console logs, network requests, and backend logs with AI assistance
350 lines • 12 kB
JavaScript
import { z } from 'zod';
import CDP from 'chrome-remote-interface';
import { BackendManager } from '../../utils/backend-manager.js';
import logger from '../../utils/logger.js';
export const BackendDebuggerAttachArgsSchema = z.object({
host: z.string().default('localhost').describe('Debug host'),
port: z.number().describe('Debug port (e.g., 9229 for Node.js)'),
processId: z.string().optional().describe('Process ID to attach to'),
type: z.enum(['node', 'deno', 'chrome']).default('node').describe('Runtime type')
});
export async function backendDebuggerAttach(args) {
const backendManager = BackendManager.getInstance();
const debuggerKey = `${args.host}:${args.port}`;
try {
logger.info('Attaching to debugger', { host: args.host, port: args.port });
// First, get available debug targets
const targets = await getDebugTargets(args.host, args.port);
if (targets.length === 0) {
throw new Error('No debug targets available. Ensure the process is running with --inspect flag.');
}
// Select target based on processId or first available
let target = targets[0];
if (args.processId) {
const found = targets.find(t => t.id === args.processId || t.title.includes(args.processId));
if (found)
target = found;
}
// Connect to the debugger
const client = await CDP({
host: args.host,
port: args.port,
target: target.id
});
// Store the client connection
backendManager.addDebuggerConnection(debuggerKey, client);
// Enable necessary domains
await client.Debugger.enable();
await client.Runtime.enable();
await client.Profiler.enable();
// Set up event listeners
setupDebuggerEventListeners(client, debuggerKey);
// Get initial state
const scripts = await client.Debugger.getScriptSource({ scriptId: '0' }).catch(() => null);
const runtimeInfo = await client.Runtime.evaluate({
expression: 'process.versions',
returnByValue: true
}).catch(() => null);
return {
success: true,
debugger: {
connected: true,
target: {
id: target.id,
title: target.title,
type: target.type,
url: target.url
},
runtime: runtimeInfo?.result?.value || {},
capabilities: {
debugging: true,
profiling: true,
heapSnapshot: args.type === 'node',
coverage: true
}
},
message: `Successfully attached to ${args.type} debugger at ${args.host}:${args.port}`
};
}
catch (error) {
logger.error('Failed to attach debugger', { error });
return {
isError: true,
error: error instanceof Error ? error.message : 'Failed to attach debugger'
};
}
}
async function getDebugTargets(host, port) {
try {
const response = await fetch(`http://${host}:${port}/json/list`);
const targets = await response.json();
return targets;
}
catch (error) {
logger.error('Failed to get debug targets', { error });
return [];
}
}
function setupDebuggerEventListeners(client, debuggerKey) {
const backendManager = BackendManager.getInstance();
// Debugger paused event
client.Debugger.paused((params) => {
logger.info('Debugger paused', {
reason: params.reason,
callFrames: params.callFrames.length
});
backendManager.addDebuggerEvent(debuggerKey, {
type: 'paused',
timestamp: new Date().toISOString(),
data: {
reason: params.reason,
callFrames: params.callFrames,
hitBreakpoints: params.hitBreakpoints
}
});
});
// Script parsed event
client.Debugger.scriptParsed((params) => {
logger.debug('Script parsed', {
scriptId: params.scriptId,
url: params.url
});
backendManager.addDebuggerScript(debuggerKey, {
scriptId: params.scriptId,
url: params.url,
startLine: params.startLine,
startColumn: params.startColumn,
endLine: params.endLine,
endColumn: params.endColumn,
hash: params.hash,
isModule: params.isModule
});
});
// Console API called
client.Runtime.consoleAPICalled((params) => {
const logEntry = {
timestamp: new Date().toISOString(),
type: params.type,
args: params.args,
stackTrace: params.stackTrace
};
backendManager.addDebuggerEvent(debuggerKey, {
type: 'console',
timestamp: logEntry.timestamp,
data: logEntry
});
});
// Exception thrown
client.Runtime.exceptionThrown((params) => {
logger.warn('Exception thrown', {
text: params.exceptionDetails.text
});
backendManager.addDebuggerEvent(debuggerKey, {
type: 'exception',
timestamp: new Date().toISOString(),
data: params.exceptionDetails
});
});
}
// Additional debugger control functions that can be exposed as separate tools
export async function setBreakpoint(debuggerKey, url, lineNumber, condition) {
const backendManager = BackendManager.getInstance();
const client = backendManager.getDebuggerConnection(debuggerKey);
if (!client) {
return { isError: true, error: 'Debugger not connected' };
}
try {
const result = await client.Debugger.setBreakpointByUrl({
lineNumber,
url,
condition
});
return {
success: true,
breakpoint: {
id: result.breakpointId,
locations: result.locations
}
};
}
catch (error) {
return {
isError: true,
error: error instanceof Error ? error.message : 'Failed to set breakpoint'
};
}
}
export async function removeBreakpoint(debuggerKey, breakpointId) {
const backendManager = BackendManager.getInstance();
const client = backendManager.getDebuggerConnection(debuggerKey);
if (!client) {
return { isError: true, error: 'Debugger not connected' };
}
try {
await client.Debugger.removeBreakpoint({ breakpointId });
return { success: true };
}
catch (error) {
return {
isError: true,
error: error instanceof Error ? error.message : 'Failed to remove breakpoint'
};
}
}
export async function stepOver(debuggerKey) {
const backendManager = BackendManager.getInstance();
const client = backendManager.getDebuggerConnection(debuggerKey);
if (!client) {
return { isError: true, error: 'Debugger not connected' };
}
try {
await client.Debugger.stepOver();
return { success: true };
}
catch (error) {
return {
isError: true,
error: error instanceof Error ? error.message : 'Failed to step over'
};
}
}
export async function stepInto(debuggerKey) {
const backendManager = BackendManager.getInstance();
const client = backendManager.getDebuggerConnection(debuggerKey);
if (!client) {
return { isError: true, error: 'Debugger not connected' };
}
try {
await client.Debugger.stepInto();
return { success: true };
}
catch (error) {
return {
isError: true,
error: error instanceof Error ? error.message : 'Failed to step into'
};
}
}
export async function stepOut(debuggerKey) {
const backendManager = BackendManager.getInstance();
const client = backendManager.getDebuggerConnection(debuggerKey);
if (!client) {
return { isError: true, error: 'Debugger not connected' };
}
try {
await client.Debugger.stepOut();
return { success: true };
}
catch (error) {
return {
isError: true,
error: error instanceof Error ? error.message : 'Failed to step out'
};
}
}
export async function resume(debuggerKey) {
const backendManager = BackendManager.getInstance();
const client = backendManager.getDebuggerConnection(debuggerKey);
if (!client) {
return { isError: true, error: 'Debugger not connected' };
}
try {
await client.Debugger.resume();
return { success: true };
}
catch (error) {
return {
isError: true,
error: error instanceof Error ? error.message : 'Failed to resume'
};
}
}
export async function evaluateExpression(debuggerKey, expression, callFrameId) {
const backendManager = BackendManager.getInstance();
const client = backendManager.getDebuggerConnection(debuggerKey);
if (!client) {
return { isError: true, error: 'Debugger not connected' };
}
try {
const params = {
expression,
generatePreview: true,
returnByValue: false
};
if (callFrameId) {
params.callFrameId = callFrameId;
}
const result = await client.Debugger.evaluateOnCallFrame(params);
return {
success: true,
result: result.result,
exceptionDetails: result.exceptionDetails
};
}
catch (error) {
return {
isError: true,
error: error instanceof Error ? error.message : 'Failed to evaluate expression'
};
}
}
export async function getCallStack(debuggerKey) {
const backendManager = BackendManager.getInstance();
const events = backendManager.getDebuggerEvents(debuggerKey);
const pausedEvent = events
.filter(e => e.type === 'paused')
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())[0];
if (!pausedEvent) {
return {
isError: true,
error: 'Debugger not paused'
};
}
return {
success: true,
callFrames: pausedEvent.data.callFrames,
reason: pausedEvent.data.reason
};
}
export async function getVariables(debuggerKey, callFrameId, scopeIndex) {
const backendManager = BackendManager.getInstance();
const client = backendManager.getDebuggerConnection(debuggerKey);
if (!client) {
return { isError: true, error: 'Debugger not connected' };
}
try {
// Get the call frame
const callStack = await getCallStack(debuggerKey);
if (callStack.isError)
return callStack;
const callFrame = callStack.callFrames.find((f) => f.callFrameId === callFrameId);
if (!callFrame) {
return { isError: true, error: 'Call frame not found' };
}
const scope = callFrame.scopeChain[scopeIndex];
if (!scope) {
return { isError: true, error: 'Scope not found' };
}
// Get properties of the scope object
const properties = await client.Runtime.getProperties({
objectId: scope.object.objectId,
ownProperties: true,
generatePreview: true
});
return {
success: true,
variables: properties.result,
scope: {
type: scope.type,
name: scope.name
}
};
}
catch (error) {
return {
isError: true,
error: error instanceof Error ? error.message : 'Failed to get variables'
};
}
}
//# sourceMappingURL=debugger-attach.js.map