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.
465 lines (464 loc) ⢠20.4 kB
JavaScript
export class NetworkTools {
page;
networkLogs = [];
consoleLogs = [];
requestHandler = () => { };
responseHandler = () => { };
consoleHandler = () => { };
pageErrorHandler = () => { };
cdpSession;
cdpRequestHandler;
cdpResponseHandler;
constructor(page) {
this.page = page;
this.setupNetworkMonitoring().catch((error) => {
try {
// console.error('[NetworkTools] Setup error:', error);
}
catch (e) {
// Silently ignore EPIPE errors when stdio is closed
}
});
}
safeLog(message) {
try {
// console.log(message);
}
catch (error) {
// Silently ignore EPIPE errors when stdio is closed
}
}
async setupNetworkMonitoring() {
try {
// Enable Chrome DevTools Protocol Network domain
this.cdpSession = await this.page.target().createCDPSession();
await this.cdpSession.send('Network.enable');
this.safeLog('[NetworkTools] CDP Network.enable() successful');
// Create handlers that we can later remove
this.cdpRequestHandler = (params) => {
const isAPI = this.isAPIRequest(params.type || 'unknown');
const log = {
timestamp: new Date(),
method: params.request.method,
url: params.request.url,
requestHeaders: params.request.headers,
requestPayload: params.request.postData,
isAPI: isAPI
};
this.networkLogs.push(log);
// Debug logging
this.safeLog(`[NetworkTools] CDP Request: ${params.request.method} ${params.request.url} - Type: "${params.type}", IsAPI: ${isAPI}`);
};
this.cdpResponseHandler = (params) => {
const log = this.networkLogs.find(l => l.url === params.response.url && !l.status);
if (log) {
log.status = params.response.status;
log.responseHeaders = params.response.headers;
log.duration = Date.now() - log.timestamp.getTime();
}
};
// Listen to Network.requestWillBeSent for all requests
this.cdpSession.on('Network.requestWillBeSent', this.cdpRequestHandler);
// Listen to Network.responseReceived for response data
this.cdpSession.on('Network.responseReceived', this.cdpResponseHandler);
this.safeLog('[NetworkTools] CDP event listeners attached');
}
catch (error) {
this.safeLog(`[NetworkTools] CDP setup failed: ${error}`);
// Fall back to Puppeteer page events if CDP fails
this.setupFallbackMonitoring();
}
// Keep console monitoring from page events
this.consoleHandler = (msg) => {
this.consoleLogs.push({
timestamp: new Date(),
type: msg.type(),
text: msg.text(),
args: msg.args(),
source: 'console'
});
};
// Add page error monitoring for JavaScript errors including throw new Error()
this.pageErrorHandler = (error) => {
this.consoleLogs.push({
timestamp: new Date(),
type: 'error',
text: `Uncaught ${error.message}`,
args: [],
source: 'pageerror',
stack: error.stack || 'No stack trace available',
line: error.line || 'unknown',
column: error.column || 'unknown',
filename: error.filename || 'unknown'
});
};
this.page.on('console', this.consoleHandler);
this.page.on('pageerror', this.pageErrorHandler);
}
setupFallbackMonitoring() {
this.safeLog('[NetworkTools] Using fallback Puppeteer monitoring');
// Fallback to original Puppeteer page events
this.requestHandler = (request) => {
const resourceType = request.resourceType();
const isAPI = this.isAPIRequest(resourceType);
const url = request.url();
const name = url.split('/').pop() || url;
const log = {
timestamp: new Date(),
method: request.method(),
url: url,
requestHeaders: request.headers(),
requestPayload: request.postData(),
isAPI: isAPI,
resourceType: resourceType,
name: name,
initiator: request.frame() ? { type: 'parser' } : { type: 'script' }
};
this.networkLogs.push(log);
this.safeLog(`[NetworkTools] Fallback Request: ${request.method()} ${request.url()} - Type: "${resourceType}", IsAPI: ${isAPI}`);
};
this.responseHandler = (response) => {
const log = this.networkLogs.find(l => l.url === response.url() && !l.status);
if (log) {
log.status = response.status();
log.responseHeaders = response.headers();
log.duration = Date.now() - log.timestamp.getTime();
// Try to get response size from headers
const contentLength = response.headers()['content-length'];
if (contentLength) {
log.size = parseInt(contentLength, 10);
}
}
};
this.page.on('request', this.requestHandler);
this.page.on('response', this.responseHandler);
}
// Cleanup method to remove event listeners
async cleanup() {
// Remove Puppeteer page event listeners
if (this.page && this.requestHandler) {
this.page.off('request', this.requestHandler);
}
if (this.page && this.responseHandler) {
this.page.off('response', this.responseHandler);
}
if (this.page && this.consoleHandler) {
this.page.off('console', this.consoleHandler);
}
if (this.page && this.pageErrorHandler) {
this.page.off('pageerror', this.pageErrorHandler);
}
// Remove CDP session listeners
if (this.cdpSession) {
if (this.cdpRequestHandler) {
this.cdpSession.off('Network.requestWillBeSent', this.cdpRequestHandler);
}
if (this.cdpResponseHandler) {
this.cdpSession.off('Network.responseReceived', this.cdpResponseHandler);
}
// Detach the CDP session
try {
await this.cdpSession.detach();
}
catch (error) {
this.safeLog(`[NetworkTools] Error detaching CDP session: ${error}`);
}
this.cdpSession = undefined;
}
this.safeLog('[NetworkTools] Cleanup completed');
}
isAPIRequest(resourceType) {
// Handle both CDP capitalized values ("Fetch", "XHR") and Puppeteer lowercase values ("fetch", "xhr")
const type = resourceType.toLowerCase();
return type === 'xhr' || type === 'fetch';
}
async getConsoleLogs(args) {
const { type } = args;
let logs = this.consoleLogs;
if (type) {
logs = logs.filter(log => log.type === type);
}
const recentLogs = logs.slice(-50); // Last 50 logs
return {
content: [
{
type: 'text',
text: `š Console Logs (${recentLogs.length} entries)\n\n` +
recentLogs.map((log, i) => {
let entry = `[${i + 1}] ${log.timestamp.toISOString()} [${log.type.toUpperCase()}]`;
if (log.source)
entry += ` (${log.source})`;
entry += `: ${log.text}`;
// Add extra details for page errors
if (log.source === 'pageerror' && log.stack) {
entry += `\n š Location: ${log.filename}:${log.line}:${log.column}`;
entry += `\n š Stack: ${log.stack.split('\n')[0]}`;
}
return entry;
}).join('\n') +
`\n\nš” Use --type parameter to filter (log, error, warning, info)`
},
],
};
}
async getNetworkLogs(args) {
const { method, status, resourceType } = args;
let logs = this.networkLogs;
if (method) {
logs = logs.filter(log => log.method.toLowerCase() === method.toLowerCase());
}
if (status) {
logs = logs.filter(log => log.status === status);
}
if (resourceType) {
// Handle special cases for fetch and xhr
if (resourceType === 'fetch' || resourceType === 'xhr') {
logs = logs.filter(log => log.resourceType === 'fetch' ||
log.resourceType === 'xhr' ||
log.resourceType === 'xmlhttprequest');
}
else {
logs = logs.filter(log => log.resourceType?.toLowerCase() === resourceType.toLowerCase());
}
}
const recentLogs = logs.slice(-20); // Last 20 requests
return {
content: [
{
type: 'text',
text: `š Network Logs (${recentLogs.length} requests)\n\n` +
recentLogs.map((log, i) => {
const name = log.name || log.url.split('/').pop() || log.url;
const size = log.size ? `${(log.size / 1024).toFixed(1)} kB` : 'N/A';
const initiator = log.initiator ? `${log.initiator.type || 'unknown'}` : 'N/A';
return `[${i + 1}] ${log.method} ${log.url}\n` +
` Status: ${log.status || 'pending'} | Type: ${log.resourceType || 'unknown'} | Duration: ${log.duration || 'N/A'}ms\n` +
` Initiator: ${initiator} | Size: ${size} | IsAPI: ${log.isAPI || false}\n`;
}).join('\n') +
`\nš” Use --method, --status, or --resourceType to filter (e.g., --resourceType fetch)`
},
],
};
}
async getAPILogs(args) {
const { method, status, urlPattern, since } = args;
let logs = this.networkLogs.filter(log => log.isAPI);
if (method) {
logs = logs.filter(log => log.method.toLowerCase() === method.toLowerCase());
}
if (status) {
logs = logs.filter(log => log.status === status);
}
if (urlPattern) {
const regex = new RegExp(urlPattern);
logs = logs.filter(log => regex.test(log.url));
}
if (since) {
const sinceDate = new Date(since);
logs = logs.filter(log => log.timestamp >= sinceDate);
}
const formattedLogs = logs.slice(-10); // Last 10 API requests
return {
content: [
{
type: 'text',
text: `š” API Request Logs (${formattedLogs.length} requests)\n\n` +
formattedLogs.map((log, i) => `[${i + 1}] ${log.method} ${log.url}\n` +
` Status: ${log.status} | Duration: ${log.duration || 'N/A'}\n` +
(log.requestPayload ? ` Payload: ${JSON.stringify(log.requestPayload)}\n` : '') +
(log.initiator?.stack ? ` Called from: ${log.initiator.stack[0]}\n` : '') +
` Response: ${log.responseBody?.substring(0, 100)}${log.responseBody && log.responseBody.length > 100 ? '...' : ''}\n`).join('\n') +
`\nš” Use specific index for full details or filter with method/urlPattern/status`
},
],
};
}
async clearLogs() {
this.networkLogs = [];
this.consoleLogs = [];
return {
content: [
{
type: 'text',
text: 'šļø Console and network logs cleared'
},
],
};
}
// Debug method to see all captured network logs with their isAPI status
async debugAllLogs() {
const logs = this.networkLogs.slice(-5); // Last 5 requests
return {
content: [
{
type: 'text',
text: `š DEBUG: Raw Network Log Objects (${logs.length} requests)\n\n` +
logs.map((log, i) => `[${i + 1}] RAW LOG DATA:\n` +
`${JSON.stringify(log, null, 2)}\n\n`).join('')
},
],
};
}
async replayAPIRequest(args) {
try {
const { url, method = 'GET', headers = {}, payload, modifyOriginal = true } = args;
// Find original request for comparison
const originalRequest = this.networkLogs.find(log => log.url === url);
// Perform the request
const response = await this.page.evaluate(async ({ url, method, headers, payload }) => {
try {
// Create AbortController with timeout to prevent memory leaks
const abortController = new AbortController();
const timeoutId = setTimeout(() => {
abortController.abort();
}, 30000); // 30 second timeout
const options = {
method,
headers: {
'Content-Type': 'application/json',
...headers
},
signal: abortController.signal // Add abort signal
};
if (payload && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
options.body = typeof payload === 'string' ? payload : JSON.stringify(payload);
}
const startTime = Date.now();
const response = await fetch(url, options);
const responseBody = await response.text();
// Clean up timeout
clearTimeout(timeoutId);
return {
status: response.status,
statusText: response.statusText,
headers: Object.fromEntries(response.headers.entries()),
body: responseBody,
duration: Date.now() - startTime
};
}
catch (error) {
return { error: error.message };
}
}, { url, method, headers, payload });
if (response.error) {
return {
content: [
{
type: 'text',
text: `ā API Request Failed\n\n` +
`Error: ${response.error}\n` +
`Request: ${method} ${url}`
},
],
};
}
const result = {
request: { method, url, headers, payload },
response
};
return {
content: [
{
type: 'text',
text: `š API Request Replay\n\n` +
`š¤ REQUEST:\n` +
`${result.request.method} ${result.request.url}\n` +
`Headers: ${JSON.stringify(result.request.headers, null, 2)}\n` +
(result.request.payload ? `Payload: ${JSON.stringify(result.request.payload, null, 2)}\n` : '') +
`\nš„ RESPONSE:\n` +
`Status: ${result.response.status} ${result.response.statusText}\n` +
`Duration: ${result.response.duration}ms\n` +
`Headers: ${JSON.stringify(result.response.headers, null, 2)}\n` +
`Body: ${typeof result.response.body === 'object' ?
JSON.stringify(result.response.body, null, 2) :
String(result.response.body)}\n\n` +
(originalRequest ?
`š Original request had: ${originalRequest.method} with status ${originalRequest.status}` :
`š No previous request found for this URL`)
},
],
};
}
catch (error) {
return {
content: [
{
type: 'text',
text: `ā Failed to replay request: ${error.message}`
},
],
};
}
}
async interceptRequests(args) {
try {
const { enable, rules = [] } = args;
if (enable) {
// Check if interception is already enabled
const isInterceptionEnabled = this.page._isInterceptionEnabled;
if (!isInterceptionEnabled) {
await this.page.setRequestInterception(true);
}
// Remove existing request listeners to avoid duplicates
this.page.removeAllListeners('request');
// Add new request handler
this.page.on('request', (request) => {
let shouldBlock = false;
let modifiedHeaders = { ...request.headers() };
let modifiedPayload = request.postData();
// Apply rules
for (const rule of rules) {
if (rule.urlPattern && new RegExp(rule.urlPattern).test(request.url())) {
if (rule.block) {
shouldBlock = true;
break;
}
if (rule.modifyHeaders) {
modifiedHeaders = { ...modifiedHeaders, ...rule.modifyHeaders };
}
if (rule.modifyPayload) {
modifiedPayload = typeof rule.modifyPayload === 'string' ?
rule.modifyPayload : JSON.stringify(rule.modifyPayload);
}
}
}
if (shouldBlock) {
request.abort();
}
else if (modifiedHeaders !== request.headers() || modifiedPayload !== request.postData()) {
request.continue({
headers: modifiedHeaders,
postData: modifiedPayload
});
}
else {
request.continue();
}
});
}
else {
await this.page.setRequestInterception(false);
}
return {
content: [
{
type: 'text',
text: enable ?
`š”ļø Request interception enabled with ${rules.length} rules` :
`š Request interception disabled`
},
],
};
}
catch (error) {
return {
content: [
{
type: 'text',
text: `ā Failed to set request interception: ${error.message}`
},
],
};
}
}
}