playwright-min-network-mcp
Version:
Minimal network monitoring MCP tool for Playwright browser automation
219 lines (218 loc) • 9.02 kB
JavaScript
/**
* Network monitoring functions for CDP integration
*/
import { randomUUID } from 'node:crypto';
/**
* Check if filter configuration is too permissive and show warnings
* @param filter Filter configuration to validate
*/
export function validateAndWarnFilter(filter) {
const warnings = [];
// Check if content types are too permissive
if (filter.contentTypes === 'all') {
warnings.push('⚠️ Content-type filter is set to "all" - this may capture many large static files');
warnings.push(' Consider using specific content types: ["application/json", "text/html"]');
}
// Check if URL filtering is too permissive
if (filter.urlIncludePatterns === 'all') {
warnings.push('⚠️ URL include patterns is set to "all" - this may capture many unnecessary requests');
warnings.push(' Consider filtering specific patterns: ["api/", "/graphql", "/v1/"] to capture only API endpoints');
}
// Check if no method filtering
if (!filter.methods || filter.methods.length === 0) {
warnings.push('⚠️ No HTTP method filtering - capturing all request methods');
warnings.push(' Consider limiting to specific methods: ["GET", "POST"]');
}
// Show warnings if any
if (warnings.length > 0) {
console.warn('🚨 Network Monitor Filter Recommendations:');
for (const warning of warnings) {
console.warn(warning);
}
console.warn(' Re-run start_or_update_capture with new filter settings to adjust filters');
}
}
/**
* Connect to Chrome DevTools Protocol WebSocket
* @param cdpUrl WebSocket URL for CDP connection
* @returns Promise<WebSocket> Connected WebSocket instance
*/
export async function connectToCdp(cdpUrl) {
return new Promise((resolve, reject) => {
const ws = new WebSocket(cdpUrl);
ws.addEventListener('open', () => {
resolve(ws);
});
ws.addEventListener('error', () => {
reject(new Error('Failed to connect to CDP'));
});
// Timeout after 5 seconds
setTimeout(() => {
if (ws.readyState !== WebSocket.OPEN) {
ws.close();
reject(new Error('Failed to connect to CDP'));
}
}, 5000);
});
}
/**
* Check if a request should be included based on URL and method filtering
* @param url Request URL
* @param method Request method
* @param filter Filter configuration
*/
export function shouldIncludeRequestByUrlAndMethod(url, method, filter) {
// Check URL include patterns
if (filter.urlIncludePatterns !== 'all') {
if (Array.isArray(filter.urlIncludePatterns)) {
let matched = false;
for (const pattern of filter.urlIncludePatterns) {
try {
const regex = new RegExp(pattern);
if (regex.test(url)) {
matched = true;
break;
}
}
catch (error) {
console.error(`Invalid URL include pattern: ${pattern}`, error);
}
}
if (!matched) {
return false;
}
}
}
// Check allowed methods
if (filter.methods && filter.methods.length > 0) {
if (!filter.methods.includes(method)) {
return false;
}
}
return true;
}
/**
* Check if a request should be included based on content type filtering
* @param contentType Response content type
* @param filter Filter configuration
*/
export function shouldIncludeRequest(contentType, filter) {
// Handle special "all" value - include everything
if (filter.contentTypes === 'all') {
return true;
}
// Handle empty array - include nothing
if (Array.isArray(filter.contentTypes) && filter.contentTypes.length === 0) {
return false;
}
// Handle content type array filtering
if (Array.isArray(filter.contentTypes) && contentType) {
return filter.contentTypes.some((ct) => contentType.includes(ct));
}
// If no content type provided, exclude by default
return false;
}
/**
* Start network monitoring by enabling Network domain and setting up event listeners
* @param ws Connected WebSocket to CDP
* @param buffer Array to store captured network requests
* @param filter Filter configuration
* @param maxBufferSize Maximum buffer size
*/
export async function startNetworkMonitoring(ws, buffer, filter, pendingRequests, maxBufferSize = 200) {
// Enable Network domain
ws.send(JSON.stringify({
id: 1,
method: 'Network.enable',
params: {},
}));
// Set up event listener for network events
ws.addEventListener('message', (event) => {
try {
const message = JSON.parse(event.data);
// Handle command responses
if (message.id === 1) {
if (message.error) {
console.error('Network.enable command failed:', message.error);
}
return;
}
// Handle Network.requestWillBeSent events
if (message.method === 'Network.requestWillBeSent') {
const { requestId, request, timestamp } = message.params;
// Apply early filtering (URL and method) before storing
if (!shouldIncludeRequestByUrlAndMethod(request.url, request.method, filter)) {
return; // Skip this request entirely
}
// Skip content-type filtering at request stage - will filter at response stage
// Create network request object
const networkRequest = {
id: requestId,
uuid: randomUUID(), // Generate UUID v4 for external reference
url: request.url,
method: request.method,
headers: request.headers,
timestamp,
type: 'request',
body: request.postData,
};
// Store in pending requests map for response-stage filtering
pendingRequests.set(requestId, networkRequest);
}
// Handle Network.responseReceived events
if (message.method === 'Network.responseReceived') {
const { requestId, response, timestamp } = message.params;
// Find existing request and add response data
const existingRequest = pendingRequests.get(requestId);
if (existingRequest) {
existingRequest.response = {
status: response.status,
headers: response.headers,
mimeType: response.mimeType,
};
existingRequest.responseTimestamp = timestamp;
// Apply content-type filtering at response stage
const shouldInclude = shouldIncludeRequest(response.mimeType, filter);
if (shouldInclude) {
// Add to final buffer with FIFO control
buffer.push(existingRequest);
if (buffer.length > maxBufferSize) {
buffer.shift();
}
// Get response body for included requests
ws.send(JSON.stringify({
id: Math.floor(Math.random() * 1000000),
method: 'Network.getResponseBody',
params: { requestId },
}));
}
// Clean up pending request
pendingRequests.delete(requestId);
}
}
// Handle Network.getResponseBody responses
if (message.result && message.result.body !== undefined) {
// Find the request that matches this response (we don't have the requestId in the response)
// This is a limitation - we'll need to match by timing or other means
// For now, we'll add the body to the most recent request without a body
const recentRequest = buffer
.slice()
.reverse()
.find((req) => req.response && !req.response.body);
if (recentRequest?.response) {
recentRequest.response.body = message.result.body;
}
}
}
catch (error) {
console.error('Error parsing CDP message:', error);
}
});
// Add error and close handlers
ws.addEventListener('error', (error) => {
console.error('WebSocket error:', error);
});
ws.addEventListener('close', (event) => {
console.error('WebSocket closed:', event.code, event.reason);
});
}