supapup
Version:
ā” Lightning-fast MCP browser dev tool. Navigate ā Get instant structured data. No screenshots needed! Puppeteer: šø ā CSS selectors ā JS eval. Supapup: semantic IDs ready to use. 10x faster, 90% fewer tokens.
414 lines (413 loc) ⢠18 kB
JavaScript
export class DebuggingTools {
page;
pausedParams = null;
client = null;
pausedHandler = () => { };
scriptParsedHandler = () => { };
constructor(page) {
this.page = page;
this.initializeCDP();
}
async initializeCDP() {
try {
// Create proper CDP session
this.client = await this.page.target().createCDPSession();
// Enable debugger domain
await this.client.send('Debugger.enable');
// Store handler for cleanup
this.pausedHandler = (params) => {
this.pausedParams = params;
const callFrame = params.callFrames[0];
// console.error(`š Breakpoint hit at ${callFrame.url}:${callFrame.location.lineNumber}`);
// console.error(`š Function: ${callFrame.functionName || 'anonymous'}`);
// console.error(`š Reason: ${params.reason}`);
};
// Handle debugger paused events
this.client.on('Debugger.paused', this.pausedHandler);
}
catch (err) {
// console.error('[DebuggingTools] CDP initialization error:', err);
}
}
// Cleanup method to remove event listeners and close CDP session
async cleanup() {
if (this.client) {
if (this.pausedHandler) {
this.client.off('Debugger.paused', this.pausedHandler);
}
if (this.scriptParsedHandler) {
this.client.off('Debugger.scriptParsed', this.scriptParsedHandler);
}
try {
await this.client.detach();
}
catch (err) {
// Ignore cleanup errors
}
this.client = null;
}
}
async setBreakpoint(args) {
try {
const { lineNumber, url = '', condition } = args;
await this.page.evaluate(() => {
if (!window.chrome?.runtime) {
window.chrome = { runtime: {} };
}
});
if (!this.client)
throw new Error('CDP session not initialized');
// Debugger already enabled in constructor
// Build location object according to CDP spec
const location = {
lineNumber: lineNumber - 1,
columnNumber: 0
};
if (url && url !== 'inline') {
// Use setBreakpointByUrl for URL-based breakpoints
const result = await this.client.send('Debugger.setBreakpointByUrl', {
lineNumber: lineNumber - 1,
url: url,
columnNumber: 0,
condition: condition
});
return {
content: [
{
type: 'text',
text: `šÆ Breakpoint set!\n\n` +
`š Location: ${url}:${lineNumber}\n` +
`š ID: ${result.breakpointId}\n` +
`ā” Condition: ${condition || 'none'}\n`
},
],
};
}
else {
// For inline scripts, we need to wait for the script to be parsed and use its scriptId
// First, enable script parsing events
await this.client.send('Debugger.enable');
// Get all parsed scripts
const scripts = [];
this.scriptParsedHandler = (params) => {
scripts.push(params);
};
this.client.on('Debugger.scriptParsed', this.scriptParsedHandler);
// Force a page evaluation to ensure scripts are parsed
await this.page.evaluate(() => { });
// Try using setBreakpointByUrl with the current page URL
// This should work for inline scripts in HTML files
const currentUrl = await this.page.url();
try {
const result = await this.client.send('Debugger.setBreakpointByUrl', {
lineNumber: lineNumber - 1,
url: currentUrl,
urlRegex: undefined,
scriptHash: undefined,
columnNumber: 0,
condition: condition
});
return {
content: [
{
type: 'text',
text: `šÆ Breakpoint set!\n\n` +
`š Location: ${currentUrl}:${lineNumber}\n` +
`š ID: ${result.breakpointId}\n` +
`š Locations: ${result.locations?.length || 0} resolved\n` +
`ā” Condition: ${condition || 'none'}\n\n` +
`ā ļø Note: For inline scripts, ensure line numbers are correct relative to the HTML file.`
},
],
};
}
catch (error) {
// If that fails, provide helpful error message
throw new Error(`Failed to set breakpoint for inline script at line ${lineNumber}. ` +
`This is a Chrome DevTools Protocol limitation. ` +
`Consider moving your script to an external file or ensure the line number is correct.`);
}
}
}
catch (error) {
throw new Error(`Failed to set breakpoint: ${error.message}`);
}
}
async removeBreakpoint(args) {
try {
const { breakpointId } = args;
if (!this.client)
throw new Error('CDP session not initialized');
await this.client.send('Debugger.removeBreakpoint', { breakpointId });
return {
content: [
{
type: 'text',
text: `šļø Breakpoint ${breakpointId} removed`
},
],
};
}
catch (error) {
throw new Error(`Failed to remove breakpoint: ${error.message}`);
}
}
async debugContinue() {
try {
if (!this.client)
throw new Error('CDP session not initialized');
await this.client.send('Debugger.resume');
this.pausedParams = null;
return {
content: [
{
type: 'text',
text: 'ā¶ļø Execution resumed\nā
Breakpoints cleaned up'
},
],
};
}
catch (error) {
throw new Error(`Failed to continue execution: ${error.message}`);
}
}
async debugStepOver() {
try {
if (!this.client)
throw new Error('CDP session not initialized');
if (!this.pausedParams) {
throw new Error('Debugger is not currently paused at a breakpoint.\n' +
'\n' +
'š To use debug_step_over, you must first:\n' +
'1. Set a breakpoint using debug_set_breakpoint\n' +
'2. Trigger code execution that hits the breakpoint\n' +
'3. Or use debug_function to automate both steps\n' +
'\n' +
'Example: debug_function({lineNumber: 10})');
}
await this.client.send('Debugger.stepOver');
return {
content: [
{
type: 'text',
text: 'š Stepped over to next line'
},
],
};
}
catch (error) {
throw new Error(`Failed to step over: ${error.message}`);
}
}
async debugStepInto() {
try {
if (!this.client)
throw new Error('CDP session not initialized');
if (!this.pausedParams) {
throw new Error('Debugger is not currently paused at a breakpoint.\n' +
'\n' +
'š To use debug_step_into, you must first:\n' +
'1. Set a breakpoint using debug_set_breakpoint\n' +
'2. Trigger code execution that hits the breakpoint\n' +
'3. Or use debug_function to automate both steps\n' +
'\n' +
'Example: debug_function({lineNumber: 10})');
}
await this.client.send('Debugger.stepInto');
return {
content: [
{
type: 'text',
text: 'š Stepped into function'
},
],
};
}
catch (error) {
throw new Error(`Failed to step into: ${error.message}`);
}
}
async debugEvaluate(args) {
try {
const { expression } = args;
if (!this.client)
throw new Error('CDP session not initialized');
if (!this.pausedParams) {
throw new Error('Debugger is not currently paused at a breakpoint.\n' +
'\n' +
'š To use debug_evaluate, you must first:\n' +
'1. Set a breakpoint using debug_set_breakpoint\n' +
'2. Trigger code execution that hits the breakpoint\n' +
'3. Or use debug_function to automate both steps\n' +
'\n' +
'Example: debug_function({lineNumber: 10})\n' +
'\n' +
'š” If you just want to evaluate JavaScript without debugging,\n' +
'use page_evaluate_script instead.');
}
const { result } = await this.client.send('Runtime.evaluate', {
expression,
contextId: this.pausedParams?.callFrames[0]?.callFrameId
});
const value = result.value !== undefined ? result.value : result.description;
return {
content: [
{
type: 'text',
text: `š Evaluation Result:\n\n` +
`š Expression: ${expression}\n` +
`š Result: ${JSON.stringify(value, null, 2)}\n` +
`š·ļø Type: ${result.type || 'unknown'}`
},
],
};
}
catch (error) {
throw new Error(`Failed to evaluate expression: ${error.message}`);
}
}
async debugGetVariables() {
try {
if (!this.pausedParams) {
throw new Error('Debugger is not currently paused at a breakpoint.\n' +
'\n' +
'š To use debug_get_variables, you must first:\n' +
'1. Set a breakpoint using debug_set_breakpoint\n' +
'2. Trigger code execution that hits the breakpoint\n' +
'3. Or use debug_function to automate both steps\n' +
'\n' +
'Example: debug_function({lineNumber: 10})\n' +
'\n' +
'š” This tool shows local variables at the current breakpoint.\n' +
'For general page inspection, use devtools_inspect_element.');
}
if (!this.client)
throw new Error('CDP session not initialized');
const callFrame = this.pausedParams.callFrames[0];
const { functionName, location } = callFrame;
const variables = {};
// Only get the first scope (local scope) to avoid massive responses
const localScope = callFrame.scopeChain.find((scope) => scope.type === 'local') || callFrame.scopeChain[0];
if (localScope) {
const { result } = await this.client.send('Runtime.getProperties', {
objectId: localScope.object.objectId,
ownProperties: true,
accessorPropertiesOnly: false,
generatePreview: false
});
// Only get simple values (not functions or complex objects)
result.forEach((prop) => {
if (prop.value && prop.value.type !== 'function' && !prop.name.startsWith('__')) {
const value = prop.value.value !== undefined ? prop.value.value :
prop.value.type === 'object' ? `[${prop.value.className || 'Object'}]` :
prop.value.description;
variables[prop.name] = value;
}
});
}
return {
content: [
{
type: 'text',
text: `š Debug Context:\n\n` +
`š Location: ${callFrame.url}:${location.lineNumber + 1}\n` +
`š Function: ${functionName || 'anonymous'}\n` +
`š Scope: ${localScope?.type || 'unknown'}\n\n` +
`š Local Variables:\n${JSON.stringify(variables, null, 2)}\n\n`
},
],
};
}
catch (error) {
throw new Error(`Failed to get variables: ${error.message}`);
}
}
async debugFunction(args) {
try {
const { lineNumber, triggerAction } = args;
// Get current page URL for breakpoint
const url = await this.page.url();
// Set breakpoint with URL
const breakpointResult = await this.setBreakpoint({ lineNumber, url });
const breakpointId = breakpointResult.content[0].text.match(/š ID: ([^\n]+)/)?.[1];
// Try to trigger the action if provided
let actionToTrigger = triggerAction;
if (!actionToTrigger) {
const actions = await this.page.evaluate(() => {
const agentPage = window.__AGENT_PAGE__;
return agentPage?.manifest?.elements?.map((el) => el.id) || [];
});
actionToTrigger = actions[0] || 'click-first-button';
}
// Execute action and wait for breakpoint
const actionPromise = this.page.evaluate((actionId) => {
const agentPage = window.__AGENT_PAGE__;
if (agentPage?.execute) {
return agentPage.execute(actionId, {});
}
return null;
}, actionToTrigger);
// Wait for debugger to pause or action to complete
const pausePromise = new Promise((resolve) => {
const checkPause = () => {
if (this.pausedParams) {
resolve(this.pausedParams);
}
else {
setTimeout(checkPause, 100);
}
};
checkPause();
});
const result = await Promise.race([
pausePromise,
actionPromise.then(() => ({ actionCompleted: true }))
]);
if (result.actionCompleted) {
return {
content: [
{
type: 'text',
text: `ā ļø Breakpoint at line ${lineNumber} was not triggered by action ${actionToTrigger}.\n` +
`The action completed without hitting the breakpoint.\n`
},
],
};
}
// Get current variables
const variablesResult = await this.debugGetVariables();
const variables = JSON.parse(variablesResult.content[0].text.split('š Local Variables:\n')[1].split('\n\n')[0]);
const callFrame = this.pausedParams.callFrames[0];
return {
content: [
{
type: 'text',
text: `š DEBUG SESSION ACTIVE\n\n` +
`š Breakpoint hit at line ${lineNumber}\n` +
`š Function: ${callFrame.functionName || 'anonymous'}\n` +
`šÆ Triggered by: ${actionToTrigger}\n\n` +
`š Current Variables:\n${JSON.stringify(variables, null, 2)}\n\n` +
`š§ NEXT STEPS:\n` +
`1. Use debug_evaluate to inspect specific expressions\n` +
` Example: debug_evaluate --expression "variableName"\n\n` +
`2. Use debug_step_over to execute next line\n` +
` Example: debug_step_over\n\n` +
`3. Use debug_continue to resume normal execution\n` +
` Example: debug_continue\n\n` +
`š” TIP: After stepping, use debug_get_variables to see updated values\n\n`
},
],
};
}
catch (error) {
return {
content: [
{
type: 'text',
text: `ā Debug setup failed: ${error.message}`
},
],
};
}
}
}