playwright-mcp/src/tools/requests.ts

561 lines
22 KiB
TypeScript
Raw Normal View History

/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { z } from 'zod';
import { defineTool } from './tool.js';
import { RequestInterceptorOptions } from '../requestInterceptor.js';
import type { Context } from '../context.js';
const startMonitoringSchema = z.object({
urlFilter: z.union([
z.string(),
z.object({
type: z.enum(['regex', 'function']),
value: z.string()
})
]).optional().describe('Filter URLs to capture. Can be a string (contains match), regex pattern, or custom function. Examples: "/api/", ".*\\.json$", or custom logic'),
captureBody: z.boolean().optional().default(true).describe('Whether to capture request and response bodies (default: true)'),
maxBodySize: z.number().optional().default(10485760).describe('Maximum body size to capture in bytes (default: 10MB). Larger bodies will be truncated'),
autoSave: z.boolean().optional().default(false).describe('Automatically save captured requests after each response (default: false for performance)'),
outputPath: z.string().optional().describe('Custom output directory path. If not specified, uses session artifact directory')
});
const getRequestsSchema = z.object({
filter: z.enum(['all', 'failed', 'slow', 'errors', 'success']).optional().default('all').describe('Filter requests by type: all, failed (network failures), slow (>1s), errors (4xx/5xx), success (2xx/3xx)'),
domain: z.string().optional().describe('Filter requests by domain hostname'),
method: z.string().optional().describe('Filter requests by HTTP method (GET, POST, etc.)'),
status: z.number().optional().describe('Filter requests by HTTP status code'),
limit: z.number().optional().default(100).describe('Maximum number of requests to return (default: 100)'),
format: z.enum(['summary', 'detailed', 'stats']).optional().default('summary').describe('Response format: summary (basic info), detailed (full data), stats (statistics only)'),
slowThreshold: z.number().optional().default(1000).describe('Threshold in milliseconds for considering requests "slow" (default: 1000ms)')
});
const exportRequestsSchema = z.object({
format: z.enum(['json', 'har', 'csv', 'summary']).optional().default('json').describe('Export format: json (full data), har (HTTP Archive), csv (spreadsheet), summary (human-readable report)'),
filename: z.string().optional().describe('Custom filename for export. Auto-generated if not specified with timestamp'),
filter: z.enum(['all', 'failed', 'slow', 'errors', 'success']).optional().default('all').describe('Filter which requests to export'),
includeBody: z.boolean().optional().default(false).describe('Include request/response bodies in export (warning: may create large files)')
});
/**
* Start comprehensive request monitoring and interception
*
* This tool enables deep HTTP traffic analysis during browser automation.
* Perfect for API reverse engineering, security testing, and performance analysis.
*
* Use Cases:
* - Security testing: Capture all API calls for vulnerability analysis
* - Performance monitoring: Identify slow endpoints and optimize
* - API documentation: Generate comprehensive API usage reports
* - Debugging: Analyze failed requests and error patterns
*/
const startRequestMonitoring = defineTool({
capability: 'core',
schema: {
name: 'browser_start_request_monitoring',
title: 'Start request monitoring',
description: 'Enable comprehensive HTTP request/response interception and analysis. Captures headers, bodies, timing, and failure information for all browser traffic. Essential for security testing, API analysis, and performance debugging.',
inputSchema: startMonitoringSchema,
type: 'destructive',
},
handle: async (context: Context, params: z.output<typeof startMonitoringSchema>, response) => {
try {
await context.ensureTab();
// Parse URL filter
let urlFilter: RequestInterceptorOptions['urlFilter'];
if (params.urlFilter) {
if (typeof params.urlFilter === 'string') {
urlFilter = params.urlFilter;
} else {
// Handle regex or function
if (params.urlFilter.type === 'regex') {
urlFilter = new RegExp(params.urlFilter.value);
} else {
// Function - evaluate safely
try {
urlFilter = eval(`(${params.urlFilter.value})`);
} catch (error: any) {
throw new Error(`Invalid filter function: ${error.message}`);
}
}
}
}
// Get output path from artifact manager or use default
let outputPath = params.outputPath;
if (!outputPath && context.sessionId) {
const artifactManager = context.getArtifactManager();
if (artifactManager)
outputPath = artifactManager.getSubdirectory('requests');
}
if (!outputPath)
outputPath = context.config.outputDir + '/requests';
const options: RequestInterceptorOptions = {
urlFilter,
captureBody: params.captureBody,
maxBodySize: params.maxBodySize,
autoSave: params.autoSave,
outputPath
};
// Start monitoring
await context.startRequestMonitoring(options);
response.addResult('✅ **Request monitoring started successfully**');
response.addResult('');
response.addResult('📊 **Configuration:**');
response.addResult(`• URL Filter: ${params.urlFilter || 'All requests'}`);
response.addResult(`• Capture Bodies: ${params.captureBody ? 'Yes' : 'No'}`);
response.addResult(`• Max Body Size: ${(params.maxBodySize! / 1024 / 1024).toFixed(1)}MB`);
response.addResult(`• Auto Save: ${params.autoSave ? 'Yes' : 'No'}`);
response.addResult(`• Output Path: ${outputPath}`);
response.addResult('');
response.addResult('🎯 **Next Steps:**');
response.addResult('1. Navigate to pages and interact with the application');
response.addResult('2. Use `browser_get_requests` to view captured traffic');
response.addResult('3. Use `browser_export_requests` to save analysis results');
response.addResult('4. Use `browser_clear_requests` to clear captured data');
} catch (error: any) {
throw new Error(`Failed to start request monitoring: ${error.message}`);
}
},
});
/**
* Retrieve and analyze captured HTTP requests
*
* Access comprehensive request data including timing, headers, bodies,
* and failure information. Supports advanced filtering and analysis.
*/
const getRequests = defineTool({
capability: 'core',
schema: {
name: 'browser_get_requests',
title: 'Get captured requests',
description: 'Retrieve and analyze captured HTTP requests with advanced filtering. Shows timing, status codes, headers, and bodies. Perfect for identifying performance issues, failed requests, or analyzing API usage patterns.',
inputSchema: getRequestsSchema,
type: 'readOnly',
},
handle: async (context: Context, params: z.output<typeof getRequestsSchema>, response) => {
try {
const interceptor = context.getRequestInterceptor();
if (!interceptor) {
response.addResult('❌ **Request monitoring not active**');
response.addResult('');
response.addResult('💡 Start monitoring first with `browser_start_request_monitoring`');
return;
}
let requests = interceptor.getData();
// Apply filters
if (params.filter !== 'all') {
switch (params.filter) {
case 'failed':
requests = interceptor.getFailedRequests();
break;
case 'slow':
requests = interceptor.getSlowRequests(params.slowThreshold);
break;
case 'errors':
requests = requests.filter(r => r.response && r.response.status >= 400);
break;
case 'success':
requests = requests.filter(r => r.response && r.response.status < 400);
break;
}
}
if (params.domain) {
requests = requests.filter(r => {
try {
return new URL(r.url).hostname === params.domain;
} catch {
return false;
}
});
}
if (params.method)
requests = requests.filter(r => r.method.toLowerCase() === params.method!.toLowerCase());
if (params.status)
requests = requests.filter(r => r.response?.status === params.status);
// Limit results
const limitedRequests = requests.slice(0, params.limit);
if (params.format === 'stats') {
// Return statistics only
const stats = interceptor.getStats();
response.addResult('📊 **Request Statistics**');
response.addResult('');
response.addResult(`• Total Requests: ${stats.totalRequests}`);
response.addResult(`• Successful: ${stats.successfulRequests} (${((stats.successfulRequests / stats.totalRequests) * 100).toFixed(1)}%)`);
response.addResult(`• Failed: ${stats.failedRequests} (${((stats.failedRequests / stats.totalRequests) * 100).toFixed(1)}%)`);
response.addResult(`• Errors: ${stats.errorResponses} (${((stats.errorResponses / stats.totalRequests) * 100).toFixed(1)}%)`);
response.addResult(`• Average Response Time: ${stats.averageResponseTime}ms`);
response.addResult(`• Slow Requests (>1s): ${stats.slowRequests}`);
response.addResult('');
response.addResult('**By Method:**');
Object.entries(stats.requestsByMethod).forEach(([method, count]) => {
response.addResult(`${method}: ${count}`);
});
response.addResult('');
response.addResult('**By Status Code:**');
Object.entries(stats.requestsByStatus).forEach(([status, count]) => {
response.addResult(`${status}: ${count}`);
});
response.addResult('');
response.addResult('**Top Domains:**');
const topDomains = Object.entries(stats.requestsByDomain)
.sort(([, a], [, b]) => b - a)
.slice(0, 5);
topDomains.forEach(([domain, count]) => {
response.addResult(`${domain}: ${count}`);
});
return;
}
// Return request data
if (limitedRequests.length === 0) {
response.addResult(' **No requests found matching the criteria**');
response.addResult('');
response.addResult('💡 Try different filters or ensure the page has made HTTP requests');
return;
}
response.addResult(`📋 **Captured Requests (${limitedRequests.length} of ${requests.length} total)**`);
response.addResult('');
limitedRequests.forEach((req, index) => {
const duration = req.duration ? `${req.duration}ms` : 'pending';
const status = req.failed ? 'FAILED' : req.response?.status || 'pending';
const size = req.response?.bodySize ? ` (${(req.response.bodySize / 1024).toFixed(1)}KB)` : '';
response.addResult(`**${index + 1}. ${req.method} ${status}** - ${duration}`);
response.addResult(` ${req.url}${size}`);
if (params.format === 'detailed') {
response.addResult(` 📅 ${req.timestamp}`);
if (req.response) {
response.addResult(` 📊 Status: ${req.response.status} ${req.response.statusText}`);
response.addResult(` ⏱️ Duration: ${req.response.duration}ms`);
response.addResult(` 🔄 From Cache: ${req.response.fromCache ? 'Yes' : 'No'}`);
// Show key headers
const contentType = req.response.headers['content-type'];
if (contentType)
response.addResult(` 📄 Content-Type: ${contentType}`);
}
if (req.failed && req.failure)
response.addResult(` ❌ Failure: ${req.failure.errorText}`);
response.addResult('');
}
});
if (requests.length > params.limit)
response.addResult(`💡 Showing first ${params.limit} results. Use higher limit or specific filters to see more.`);
} catch (error: any) {
throw new Error(`Failed to get requests: ${error.message}`);
}
},
});
/**
* Export captured requests to various formats for external analysis
*/
const exportRequests = defineTool({
capability: 'core',
schema: {
name: 'browser_export_requests',
title: 'Export captured requests',
description: 'Export captured HTTP requests to various formats (JSON, HAR, CSV, or summary report). Perfect for sharing analysis results, importing into other tools, or creating audit reports.',
inputSchema: exportRequestsSchema,
type: 'readOnly',
},
handle: async (context: Context, params: z.output<typeof exportRequestsSchema>, response) => {
try {
const interceptor = context.getRequestInterceptor();
if (!interceptor) {
response.addResult('❌ **Request monitoring not active**');
response.addResult('');
response.addResult('💡 Start monitoring first with `browser_start_request_monitoring`');
return;
}
const requests = interceptor.getData();
if (requests.length === 0) {
response.addResult(' **No requests to export**');
response.addResult('');
response.addResult('💡 Navigate to pages and interact with the application first');
return;
}
let exportPath: string;
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const defaultFilename = `requests-${timestamp}`;
switch (params.format) {
case 'har':
exportPath = await interceptor.exportHAR(params.filename || `${defaultFilename}.har`);
break;
case 'json':
exportPath = await interceptor.save(params.filename || `${defaultFilename}.json`);
break;
case 'csv':
// Create CSV export
const csvData = requests.map(req => ({
timestamp: req.timestamp,
method: req.method,
url: req.url,
status: req.response?.status || (req.failed ? 'FAILED' : 'PENDING'),
duration: req.duration || '',
size: req.response?.bodySize || '',
contentType: req.response?.headers['content-type'] || '',
fromCache: req.response?.fromCache || false
}));
const csvHeaders = Object.keys(csvData[0]).join(',');
const csvRows = csvData.map(row => Object.values(row).join(','));
const csvContent = [csvHeaders, ...csvRows].join('\n');
const csvFilename = params.filename || `${defaultFilename}.csv`;
const csvPath = `${interceptor.getStatus().options.outputPath}/${csvFilename}`;
await require('fs/promises').writeFile(csvPath, csvContent);
exportPath = csvPath;
break;
case 'summary':
// Create human-readable summary
const stats = interceptor.getStats();
const summaryLines = [
'# HTTP Request Analysis Summary',
`Generated: ${new Date().toISOString()}`,
'',
'## Overview',
`- Total Requests: ${stats.totalRequests}`,
`- Successful: ${stats.successfulRequests}`,
`- Failed: ${stats.failedRequests}`,
`- Errors: ${stats.errorResponses}`,
`- Average Response Time: ${stats.averageResponseTime}ms`,
'',
'## Performance',
`- Fast Requests (<1s): ${stats.fastRequests}`,
`- Slow Requests (>1s): ${stats.slowRequests}`,
'',
'## Request Methods',
...Object.entries(stats.requestsByMethod).map(([method, count]) => `- ${method}: ${count}`),
'',
'## Status Codes',
...Object.entries(stats.requestsByStatus).map(([status, count]) => `- ${status}: ${count}`),
'',
'## Top Domains',
...Object.entries(stats.requestsByDomain)
.sort(([, a], [, b]) => b - a)
.slice(0, 10)
.map(([domain, count]) => `- ${domain}: ${count}`),
'',
'## Slow Requests (>1s)',
...interceptor.getSlowRequests().map(req =>
`- ${req.method} ${req.url} (${req.duration}ms)`
),
'',
'## Failed Requests',
...interceptor.getFailedRequests().map(req =>
`- ${req.method} ${req.url} (${req.response?.status || 'NETWORK_FAILED'})`
)
];
const summaryFilename = params.filename || `${defaultFilename}-summary.md`;
const summaryPath = `${interceptor.getStatus().options.outputPath}/${summaryFilename}`;
await require('fs/promises').writeFile(summaryPath, summaryLines.join('\n'));
exportPath = summaryPath;
break;
default:
throw new Error(`Unsupported export format: ${params.format}`);
}
response.addResult('✅ **Export completed successfully**');
response.addResult('');
response.addResult(`📁 **File saved:** ${exportPath}`);
response.addResult(`📊 **Exported:** ${requests.length} requests`);
response.addResult(`🗂️ **Format:** ${params.format.toUpperCase()}`);
response.addResult('');
if (params.format === 'har') {
response.addResult('💡 **HAR files** can be imported into:');
response.addResult(' • Chrome DevTools (Network tab)');
response.addResult(' • Insomnia or Postman');
response.addResult(' • Online HAR viewers');
} else if (params.format === 'json') {
response.addResult('💡 **JSON files** contain full request/response data');
response.addResult(' • Perfect for programmatic analysis');
response.addResult(' • Includes headers, bodies, timing info');
}
} catch (error: any) {
throw new Error(`Failed to export requests: ${error.message}`);
}
},
});
/**
* Clear all captured request data from memory
*/
const clearRequests = defineTool({
capability: 'core',
schema: {
name: 'browser_clear_requests',
title: 'Clear captured requests',
description: 'Clear all captured HTTP request data from memory. Useful for freeing up memory during long sessions or when starting fresh analysis.',
inputSchema: z.object({}),
type: 'destructive',
},
handle: async (context: Context, params, response) => {
try {
const interceptor = context.getRequestInterceptor();
if (!interceptor) {
response.addResult(' **Request monitoring not active**');
response.addResult('');
response.addResult('💡 Start monitoring first with `browser_start_request_monitoring`');
return;
}
const clearedCount = interceptor.clear();
response.addResult('✅ **Request data cleared successfully**');
response.addResult('');
response.addResult(`🗑️ **Cleared:** ${clearedCount} captured requests`);
response.addResult('');
response.addResult('💡 **Memory freed** - Ready for new monitoring session');
} catch (error: any) {
throw new Error(`Failed to clear requests: ${error.message}`);
}
},
});
/**
* Get current request monitoring status and configuration
*/
const getMonitoringStatus = defineTool({
capability: 'core',
schema: {
name: 'browser_request_monitoring_status',
title: 'Get request monitoring status',
description: 'Check if request monitoring is active and view current configuration. Shows capture statistics, filter settings, and output paths.',
inputSchema: z.object({}),
type: 'readOnly',
},
handle: async (context: Context, params, response) => {
try {
const interceptor = context.getRequestInterceptor();
if (!interceptor) {
response.addResult('❌ **Request monitoring is not active**');
response.addResult('');
response.addResult('💡 **To start monitoring:**');
response.addResult('1. Use `browser_start_request_monitoring` to enable');
response.addResult('2. Navigate to pages and perform actions');
response.addResult('3. Use `browser_get_requests` to view captured data');
return;
}
const status = interceptor.getStatus();
const stats = interceptor.getStats();
response.addResult('✅ **Request monitoring is active**');
response.addResult('');
response.addResult('📊 **Current Statistics:**');
response.addResult(`• Total Captured: ${stats.totalRequests} requests`);
response.addResult(`• Successful: ${stats.successfulRequests}`);
response.addResult(`• Failed: ${stats.failedRequests}`);
response.addResult(`• Average Response Time: ${stats.averageResponseTime}ms`);
response.addResult('');
response.addResult('⚙️ **Configuration:**');
response.addResult(`• Attached to Page: ${status.isAttached ? 'Yes' : 'No'}`);
response.addResult(`• Current Page: ${status.pageUrl || 'None'}`);
response.addResult(`• Capture Bodies: ${status.options.captureBody ? 'Yes' : 'No'}`);
response.addResult(`• Max Body Size: ${status.options.maxBodySize ? (status.options.maxBodySize / 1024 / 1024).toFixed(1) + 'MB' : 'Unlimited'}`);
response.addResult(`• Auto Save: ${status.options.autoSave ? 'Yes' : 'No'}`);
response.addResult(`• Output Path: ${status.options.outputPath || 'Default'}`);
if (stats.totalRequests > 0) {
response.addResult('');
response.addResult('📈 **Recent Activity:**');
const recentRequests = interceptor.getData().slice(-3);
recentRequests.forEach((req, index) => {
const duration = req.duration ? ` (${req.duration}ms)` : '';
const status = req.failed ? 'FAILED' : req.response?.status || 'pending';
response.addResult(` ${index + 1}. ${req.method} ${status} - ${new URL(req.url).pathname}${duration}`);
});
}
} catch (error: any) {
throw new Error(`Failed to get monitoring status: ${error.message}`);
}
},
});
export default [
startRequestMonitoring,
getRequests,
exportRequests,
clearRequests,
getMonitoringStatus,
];