/** * 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 { paginationParamsSchema, withPagination } from '../pagination.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 = paginationParamsSchema.extend({ 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'), 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, 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 pagination support. Shows timing, status codes, headers, and bodies. Large request lists are automatically paginated for better performance.', inputSchema: getRequestsSchema, type: 'readOnly', }, handle: async (context: Context, params: z.output, 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; } // Special case for stats format - no pagination needed if (params.format === 'stats') { 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; } // Use pagination for request data await withPagination( 'browser_get_requests', params, context, response, { maxResponseTokens: 8000, defaultPageSize: 25, // Smaller default for detailed request data dataExtractor: async () => { 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); return requests; }, itemFormatter: (req, format) => { 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)` : ''; let result = `**${req.method} ${status}** - ${duration}\n ${req.url}${size}`; if (format === 'detailed') { result += `\n 📅 ${req.timestamp}`; if (req.response) { result += `\n 📊 Status: ${req.response.status} ${req.response.statusText}`; result += `\n âąī¸ Duration: ${req.response.duration}ms`; result += `\n 🔄 From Cache: ${req.response.fromCache ? 'Yes' : 'No'}`; // Show key headers const contentType = req.response.headers['content-type']; if (contentType) result += `\n 📄 Content-Type: ${contentType}`; } if (req.failed && req.failure) result += `\n ❌ Failure: ${req.failure.errorText}`; result += '\n'; } return result; }, sessionIdExtractor: () => context.sessionId, positionCalculator: (items, lastIndex) => ({ lastIndex, totalItems: items.length, timestamp: Date.now() }) } ); } 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, 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, ];