import { createErrorResponse, ToolResult } from '@/common/tool-handler'; import { BaseBrowserToolExecutor } from '../base-browser'; import { TOOL_NAMES } from 'chrome-mcp-shared'; import { LIMITS, NETWORK_FILTERS } from '@/common/constants'; // Static resource file extensions const STATIC_RESOURCE_EXTENSIONS = [ '.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp', '.ico', '.bmp', // Images '.css', '.scss', '.less', // Styles '.js', '.jsx', '.ts', '.tsx', // Scripts '.woff', '.woff2', '.ttf', '.eot', '.otf', // Fonts '.mp3', '.mp4', '.avi', '.mov', '.wmv', '.flv', '.ogg', '.wav', // Media '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', // Documents ]; // Ad and analytics domain list const AD_ANALYTICS_DOMAINS = NETWORK_FILTERS.EXCLUDED_DOMAINS; interface NetworkCaptureStartToolParams { url?: string; // URL to navigate to or focus. If not provided, uses active tab. maxCaptureTime?: number; // Maximum capture time (milliseconds) inactivityTimeout?: number; // Inactivity timeout (milliseconds) includeStatic?: boolean; // Whether to include static resources } interface NetworkRequestInfo { requestId: string; url: string; method: string; type: string; requestTime: number; requestHeaders?: Record; requestBody?: string; responseHeaders?: Record; responseTime?: number; status?: number; statusText?: string; responseSize?: number; responseType?: string; responseBody?: string; errorText?: string; specificRequestHeaders?: Record; specificResponseHeaders?: Record; mimeType?: string; // Response MIME type } interface CaptureInfo { tabId: number; tabUrl: string; tabTitle: string; startTime: number; endTime?: number; requests: Record; maxCaptureTime: number; inactivityTimeout: number; includeStatic: boolean; limitReached?: boolean; // Whether request count limit is reached } /** * Network Capture Start Tool V2 - Uses Chrome webRequest API to start capturing network requests */ class NetworkCaptureStartTool extends BaseBrowserToolExecutor { name = TOOL_NAMES.BROWSER.NETWORK_CAPTURE_START; public static instance: NetworkCaptureStartTool | null = null; public captureData: Map = new Map(); // tabId -> capture data private captureTimers: Map = new Map(); // tabId -> max capture timer private inactivityTimers: Map = new Map(); // tabId -> inactivity timer private lastActivityTime: Map = new Map(); // tabId -> timestamp of last activity private requestCounters: Map = new Map(); // tabId -> count of captured requests public static MAX_REQUESTS_PER_CAPTURE = LIMITS.MAX_NETWORK_REQUESTS; // Maximum capture request count private listeners: { [key: string]: (details: any) => void } = {}; // Static resource MIME types list (for filtering) private static STATIC_MIME_TYPES_TO_FILTER = [ 'image/', // All image types 'font/', // All font types 'audio/', // All audio types 'video/', // All video types 'text/css', 'text/javascript', 'application/javascript', 'application/x-javascript', 'application/pdf', 'application/zip', 'application/octet-stream', // Usually for downloads or generic binary data ]; // API response MIME types list (these types are usually not filtered) private static API_MIME_TYPES = [ 'application/json', 'application/xml', 'text/xml', 'application/x-www-form-urlencoded', 'application/graphql', 'application/grpc', 'application/protobuf', 'application/x-protobuf', 'application/x-json', 'application/ld+json', 'application/problem+json', 'application/problem+xml', 'application/soap+xml', 'application/vnd.api+json', ]; constructor() { super(); if (NetworkCaptureStartTool.instance) { return NetworkCaptureStartTool.instance; } NetworkCaptureStartTool.instance = this; // Listen for tab close events chrome.tabs.onRemoved.addListener(this.handleTabRemoved.bind(this)); // Listen for tab creation events chrome.tabs.onCreated.addListener(this.handleTabCreated.bind(this)); } /** * Handle tab close events */ private handleTabRemoved(tabId: number) { if (this.captureData.has(tabId)) { console.log(`NetworkCaptureV2: Tab ${tabId} was closed, cleaning up resources.`); this.cleanupCapture(tabId); } } /** * Handle tab creation events * If a new tab is opened from a tab being captured, automatically start capturing the new tab's requests */ private async handleTabCreated(tab: chrome.tabs.Tab) { try { // Check if there are any tabs currently capturing if (this.captureData.size === 0) return; // Get the openerTabId of the new tab (ID of the tab that opened this tab) const openerTabId = tab.openerTabId; if (!openerTabId) return; // Check if the opener tab is currently capturing if (!this.captureData.has(openerTabId)) return; // Get the new tab's ID const newTabId = tab.id; if (!newTabId) return; console.log( `NetworkCaptureV2: New tab ${newTabId} created from capturing tab ${openerTabId}, will extend capture to it.`, ); // Get the opener tab's capture settings const openerCaptureInfo = this.captureData.get(openerTabId); if (!openerCaptureInfo) return; // Wait a short time to ensure the tab is ready await new Promise((resolve) => setTimeout(resolve, 500)); // Start capturing requests for the new tab await this.startCaptureForTab(newTabId, { maxCaptureTime: openerCaptureInfo.maxCaptureTime, inactivityTimeout: openerCaptureInfo.inactivityTimeout, includeStatic: openerCaptureInfo.includeStatic, }); console.log(`NetworkCaptureV2: Successfully extended capture to new tab ${newTabId}`); } catch (error) { console.error(`NetworkCaptureV2: Error extending capture to new tab:`, error); } } /** * Determine whether a request should be filtered (based on URL) */ private shouldFilterRequest(url: string, includeStatic: boolean): boolean { try { const urlObj = new URL(url); // Check if it's an ad or analytics domain if (AD_ANALYTICS_DOMAINS.some((domain) => urlObj.hostname.includes(domain))) { console.log(`NetworkCaptureV2: Filtering ad/analytics domain: ${urlObj.hostname}`); return true; } // If not including static resources, check extensions if (!includeStatic) { const path = urlObj.pathname.toLowerCase(); if (STATIC_RESOURCE_EXTENSIONS.some((ext) => path.endsWith(ext))) { console.log(`NetworkCaptureV2: Filtering static resource by extension: ${path}`); return true; } } return false; } catch (e) { console.error('NetworkCaptureV2: Error filtering URL:', e); return false; } } /** * Filter based on MIME type */ private shouldFilterByMimeType(mimeType: string, includeStatic: boolean): boolean { if (!mimeType) return false; // Always keep API response types if (NetworkCaptureStartTool.API_MIME_TYPES.some((type) => mimeType.startsWith(type))) { return false; } // If not including static resources, filter out static resource MIME types if (!includeStatic) { // Filter static resource MIME types if ( NetworkCaptureStartTool.STATIC_MIME_TYPES_TO_FILTER.some((type) => mimeType.startsWith(type), ) ) { console.log(`NetworkCaptureV2: Filtering static resource by MIME type: ${mimeType}`); return true; } // Filter all MIME types starting with text/ (except those already in API_MIME_TYPES) if (mimeType.startsWith('text/')) { console.log(`NetworkCaptureV2: Filtering text response: ${mimeType}`); return true; } } return false; } /** * Update last activity time and reset inactivity timer */ private updateLastActivityTime(tabId: number): void { const captureInfo = this.captureData.get(tabId); if (!captureInfo) return; this.lastActivityTime.set(tabId, Date.now()); // Reset inactivity timer if (this.inactivityTimers.has(tabId)) { clearTimeout(this.inactivityTimers.get(tabId)!); } if (captureInfo.inactivityTimeout > 0) { this.inactivityTimers.set( tabId, setTimeout(() => this.checkInactivity(tabId), captureInfo.inactivityTimeout), ); } } /** * Check for inactivity */ private checkInactivity(tabId: number): void { const captureInfo = this.captureData.get(tabId); if (!captureInfo) return; const lastActivity = this.lastActivityTime.get(tabId) || captureInfo.startTime; const now = Date.now(); const inactiveTime = now - lastActivity; if (inactiveTime >= captureInfo.inactivityTimeout) { console.log( `NetworkCaptureV2: No activity for ${inactiveTime}ms, stopping capture for tab ${tabId}`, ); this.stopCaptureByInactivity(tabId); } else { // If inactivity time hasn't been reached yet, continue checking const remainingTime = captureInfo.inactivityTimeout - inactiveTime; this.inactivityTimers.set( tabId, setTimeout(() => this.checkInactivity(tabId), remainingTime), ); } } /** * Stop capture due to inactivity */ private async stopCaptureByInactivity(tabId: number): Promise { const captureInfo = this.captureData.get(tabId); if (!captureInfo) return; console.log(`NetworkCaptureV2: Stopping capture due to inactivity for tab ${tabId}`); await this.stopCapture(tabId); } /** * Clean up capture resources */ private cleanupCapture(tabId: number): void { // Clear timers if (this.captureTimers.has(tabId)) { clearTimeout(this.captureTimers.get(tabId)!); this.captureTimers.delete(tabId); } if (this.inactivityTimers.has(tabId)) { clearTimeout(this.inactivityTimers.get(tabId)!); this.inactivityTimers.delete(tabId); } // Remove data this.lastActivityTime.delete(tabId); this.captureData.delete(tabId); this.requestCounters.delete(tabId); console.log(`NetworkCaptureV2: Cleaned up all resources for tab ${tabId}`); } /** * Set up request listeners */ private setupListeners(): void { // Before request is sent this.listeners.onBeforeRequest = (details: chrome.webRequest.WebRequestBodyDetails) => { const captureInfo = this.captureData.get(details.tabId); if (!captureInfo) return; if (this.shouldFilterRequest(details.url, captureInfo.includeStatic)) { return; } const currentCount = this.requestCounters.get(details.tabId) || 0; if (currentCount >= NetworkCaptureStartTool.MAX_REQUESTS_PER_CAPTURE) { console.log( `NetworkCaptureV2: Request limit (${NetworkCaptureStartTool.MAX_REQUESTS_PER_CAPTURE}) reached for tab ${details.tabId}, ignoring new request: ${details.url}`, ); captureInfo.limitReached = true; return; } this.requestCounters.set(details.tabId, currentCount + 1); this.updateLastActivityTime(details.tabId); if (!captureInfo.requests[details.requestId]) { captureInfo.requests[details.requestId] = { requestId: details.requestId, url: details.url, method: details.method, type: details.type, requestTime: details.timeStamp, }; if (details.requestBody) { const requestBody = this.processRequestBody(details.requestBody); if (requestBody) { captureInfo.requests[details.requestId].requestBody = requestBody; } } console.log( `NetworkCaptureV2: Captured request ${currentCount + 1}/${NetworkCaptureStartTool.MAX_REQUESTS_PER_CAPTURE} for tab ${details.tabId}: ${details.method} ${details.url}`, ); } }; // Send request headers this.listeners.onSendHeaders = (details: chrome.webRequest.WebRequestHeadersDetails) => { const captureInfo = this.captureData.get(details.tabId); if (!captureInfo || !captureInfo.requests[details.requestId]) return; if (details.requestHeaders) { const headers: Record = {}; details.requestHeaders.forEach((header) => { headers[header.name] = header.value || ''; }); captureInfo.requests[details.requestId].requestHeaders = headers; } }; // Receive response headers this.listeners.onHeadersReceived = (details: chrome.webRequest.WebResponseHeadersDetails) => { const captureInfo = this.captureData.get(details.tabId); if (!captureInfo || !captureInfo.requests[details.requestId]) return; const requestInfo = captureInfo.requests[details.requestId]; requestInfo.status = details.statusCode; requestInfo.statusText = details.statusLine; requestInfo.responseTime = details.timeStamp; requestInfo.mimeType = details.responseHeaders?.find( (h) => h.name.toLowerCase() === 'content-type', )?.value; // Secondary filtering based on MIME type if ( requestInfo.mimeType && this.shouldFilterByMimeType(requestInfo.mimeType, captureInfo.includeStatic) ) { delete captureInfo.requests[details.requestId]; const currentCount = this.requestCounters.get(details.tabId) || 0; if (currentCount > 0) { this.requestCounters.set(details.tabId, currentCount - 1); } console.log( `NetworkCaptureV2: Filtered request by MIME type (${requestInfo.mimeType}): ${requestInfo.url}`, ); return; } if (details.responseHeaders) { const headers: Record = {}; details.responseHeaders.forEach((header) => { headers[header.name] = header.value || ''; }); requestInfo.responseHeaders = headers; } this.updateLastActivityTime(details.tabId); }; // Request completed this.listeners.onCompleted = (details: chrome.webRequest.WebResponseCacheDetails) => { const captureInfo = this.captureData.get(details.tabId); if (!captureInfo || !captureInfo.requests[details.requestId]) return; const requestInfo = captureInfo.requests[details.requestId]; if ('responseSize' in details) { requestInfo.responseSize = details.fromCache ? 0 : (details as any).responseSize; } this.updateLastActivityTime(details.tabId); }; // Request failed this.listeners.onErrorOccurred = (details: chrome.webRequest.WebResponseErrorDetails) => { const captureInfo = this.captureData.get(details.tabId); if (!captureInfo || !captureInfo.requests[details.requestId]) return; const requestInfo = captureInfo.requests[details.requestId]; requestInfo.errorText = details.error; this.updateLastActivityTime(details.tabId); }; // Register all listeners chrome.webRequest.onBeforeRequest.addListener( this.listeners.onBeforeRequest, { urls: [''] }, ['requestBody'], ); chrome.webRequest.onSendHeaders.addListener( this.listeners.onSendHeaders, { urls: [''] }, ['requestHeaders'], ); chrome.webRequest.onHeadersReceived.addListener( this.listeners.onHeadersReceived, { urls: [''] }, ['responseHeaders'], ); chrome.webRequest.onCompleted.addListener(this.listeners.onCompleted, { urls: [''] }); chrome.webRequest.onErrorOccurred.addListener(this.listeners.onErrorOccurred, { urls: [''], }); } /** * Remove all request listeners * Only remove listeners when all tab captures have stopped */ private removeListeners(): void { // Don't remove listeners if there are still tabs being captured if (this.captureData.size > 0) { console.log( `NetworkCaptureV2: Still capturing on ${this.captureData.size} tabs, not removing listeners.`, ); return; } console.log(`NetworkCaptureV2: No more active captures, removing all listeners.`); if (this.listeners.onBeforeRequest) { chrome.webRequest.onBeforeRequest.removeListener(this.listeners.onBeforeRequest); } if (this.listeners.onSendHeaders) { chrome.webRequest.onSendHeaders.removeListener(this.listeners.onSendHeaders); } if (this.listeners.onHeadersReceived) { chrome.webRequest.onHeadersReceived.removeListener(this.listeners.onHeadersReceived); } if (this.listeners.onCompleted) { chrome.webRequest.onCompleted.removeListener(this.listeners.onCompleted); } if (this.listeners.onErrorOccurred) { chrome.webRequest.onErrorOccurred.removeListener(this.listeners.onErrorOccurred); } // Clear listener object this.listeners = {}; } /** * Process request body data */ private processRequestBody(requestBody: chrome.webRequest.WebRequestBody): string | undefined { if (requestBody.raw && requestBody.raw.length > 0) { return '[Binary data]'; } else if (requestBody.formData) { return JSON.stringify(requestBody.formData); } return undefined; } /** * Start network request capture for specified tab * @param tabId Tab ID * @param options Capture options */ private async startCaptureForTab( tabId: number, options: { maxCaptureTime: number; inactivityTimeout: number; includeStatic: boolean; }, ): Promise { const { maxCaptureTime, inactivityTimeout, includeStatic } = options; // If already capturing, stop first if (this.captureData.has(tabId)) { console.log( `NetworkCaptureV2: Already capturing on tab ${tabId}. Stopping previous session.`, ); await this.stopCapture(tabId); } try { // Get tab information const tab = await chrome.tabs.get(tabId); // Initialize capture data this.captureData.set(tabId, { tabId: tabId, tabUrl: tab.url || '', tabTitle: tab.title || '', startTime: Date.now(), requests: {}, maxCaptureTime, inactivityTimeout, includeStatic, limitReached: false, }); // Initialize request counter this.requestCounters.set(tabId, 0); // Set up listeners this.setupListeners(); // Update last activity time this.updateLastActivityTime(tabId); console.log( `NetworkCaptureV2: Started capture for tab ${tabId} (${tab.url}). Max requests: ${NetworkCaptureStartTool.MAX_REQUESTS_PER_CAPTURE}, Max time: ${maxCaptureTime}ms, Inactivity: ${inactivityTimeout}ms.`, ); // Set maximum capture time if (maxCaptureTime > 0) { this.captureTimers.set( tabId, setTimeout(async () => { console.log( `NetworkCaptureV2: Max capture time (${maxCaptureTime}ms) reached for tab ${tabId}.`, ); await this.stopCapture(tabId); }, maxCaptureTime), ); } } catch (error: any) { console.error(`NetworkCaptureV2: Error starting capture for tab ${tabId}:`, error); // Clean up resources if (this.captureData.has(tabId)) { this.cleanupCapture(tabId); } throw error; } } /** * Stop capture * @param tabId Tab ID */ public async stopCapture( tabId: number, ): Promise<{ success: boolean; message?: string; data?: any }> { const captureInfo = this.captureData.get(tabId); if (!captureInfo) { console.log(`NetworkCaptureV2: No capture in progress for tab ${tabId}`); return { success: false, message: `No capture in progress for tab ${tabId}` }; } try { // Record end time captureInfo.endTime = Date.now(); // Extract common request and response headers const requestsArray = Object.values(captureInfo.requests); const commonRequestHeaders = this.analyzeCommonHeaders(requestsArray, 'requestHeaders'); const commonResponseHeaders = this.analyzeCommonHeaders(requestsArray, 'responseHeaders'); // Process request data, remove common headers const processedRequests = requestsArray.map((req) => { const finalReq: NetworkRequestInfo = { ...req }; if (finalReq.requestHeaders) { finalReq.specificRequestHeaders = this.filterOutCommonHeaders( finalReq.requestHeaders, commonRequestHeaders, ); delete finalReq.requestHeaders; } else { finalReq.specificRequestHeaders = {}; } if (finalReq.responseHeaders) { finalReq.specificResponseHeaders = this.filterOutCommonHeaders( finalReq.responseHeaders, commonResponseHeaders, ); delete finalReq.responseHeaders; } else { finalReq.specificResponseHeaders = {}; } return finalReq; }); // Sort by time processedRequests.sort((a, b) => (a.requestTime || 0) - (b.requestTime || 0)); // Remove listeners this.removeListeners(); // Prepare result data const resultData = { captureStartTime: captureInfo.startTime, captureEndTime: captureInfo.endTime, totalDurationMs: captureInfo.endTime - captureInfo.startTime, settingsUsed: { maxCaptureTime: captureInfo.maxCaptureTime, inactivityTimeout: captureInfo.inactivityTimeout, includeStatic: captureInfo.includeStatic, maxRequests: NetworkCaptureStartTool.MAX_REQUESTS_PER_CAPTURE, }, commonRequestHeaders, commonResponseHeaders, requests: processedRequests, requestCount: processedRequests.length, totalRequestsReceived: this.requestCounters.get(tabId) || 0, requestLimitReached: captureInfo.limitReached || false, tabUrl: captureInfo.tabUrl, tabTitle: captureInfo.tabTitle, }; // Clean up resources this.cleanupCapture(tabId); return { success: true, data: resultData, }; } catch (error: any) { console.error(`NetworkCaptureV2: Error stopping capture for tab ${tabId}:`, error); // Ensure resources are cleaned up this.cleanupCapture(tabId); return { success: false, message: `Error stopping capture: ${error.message || String(error)}`, }; } } /** * Analyze common request or response headers */ private analyzeCommonHeaders( requests: NetworkRequestInfo[], headerType: 'requestHeaders' | 'responseHeaders', ): Record { if (!requests || requests.length === 0) return {}; // Find headers that are included in all requests const commonHeaders: Record = {}; const firstRequestWithHeaders = requests.find( (req) => req[headerType] && Object.keys(req[headerType] || {}).length > 0, ); if (!firstRequestWithHeaders || !firstRequestWithHeaders[headerType]) { return {}; } // Get all headers from the first request const headers = firstRequestWithHeaders[headerType] as Record; const headerNames = Object.keys(headers); // Check if each header exists in all requests with the same value for (const name of headerNames) { const value = headers[name]; const isCommon = requests.every((req) => { const reqHeaders = req[headerType] as Record; return reqHeaders && reqHeaders[name] === value; }); if (isCommon) { commonHeaders[name] = value; } } return commonHeaders; } /** * Filter out common headers */ private filterOutCommonHeaders( headers: Record, commonHeaders: Record, ): Record { if (!headers || typeof headers !== 'object') return {}; const specificHeaders: Record = {}; // Use Object.keys to avoid ESLint no-prototype-builtins warning Object.keys(headers).forEach((name) => { if (!(name in commonHeaders) || headers[name] !== commonHeaders[name]) { specificHeaders[name] = headers[name]; } }); return specificHeaders; } async execute(args: NetworkCaptureStartToolParams): Promise { const { url: targetUrl, maxCaptureTime = 3 * 60 * 1000, // Default 3 minutes inactivityTimeout = 60 * 1000, // Default 1 minute of inactivity before auto-stop includeStatic = false, // Default: don't include static resources } = args; console.log(`NetworkCaptureStartTool: Executing with args:`, args); try { // Get current tab or create new tab let tabToOperateOn: chrome.tabs.Tab; if (targetUrl) { // Find tabs matching the URL const matchingTabs = await chrome.tabs.query({ url: targetUrl }); if (matchingTabs.length > 0) { // Use existing tab tabToOperateOn = matchingTabs[0]; console.log(`NetworkCaptureV2: Found existing tab with URL: ${targetUrl}`); } else { // Create new tab console.log(`NetworkCaptureV2: Creating new tab with URL: ${targetUrl}`); tabToOperateOn = await chrome.tabs.create({ url: targetUrl, active: true }); // Wait for page to load await new Promise((resolve) => setTimeout(resolve, 1000)); } } else { // Use current active tab const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); if (!tabs[0]) { return createErrorResponse('No active tab found'); } tabToOperateOn = tabs[0]; } if (!tabToOperateOn?.id) { return createErrorResponse('Failed to identify or create a tab'); } // Use startCaptureForTab method to start capture try { await this.startCaptureForTab(tabToOperateOn.id, { maxCaptureTime, inactivityTimeout, includeStatic, }); } catch (error: any) { return createErrorResponse( `Failed to start capture for tab ${tabToOperateOn.id}: ${error.message || String(error)}`, ); } return { content: [ { type: 'text', text: JSON.stringify({ success: true, message: 'Network capture V2 started successfully, waiting for stop command.', tabId: tabToOperateOn.id, url: tabToOperateOn.url, maxCaptureTime, inactivityTimeout, includeStatic, maxRequests: NetworkCaptureStartTool.MAX_REQUESTS_PER_CAPTURE, }), }, ], isError: false, }; } catch (error: any) { console.error('NetworkCaptureStartTool: Critical error:', error); return createErrorResponse( `Error in NetworkCaptureStartTool: ${error.message || String(error)}`, ); } } } /** * Network capture stop tool V2 - Stop webRequest API capture and return results */ class NetworkCaptureStopTool extends BaseBrowserToolExecutor { name = TOOL_NAMES.BROWSER.NETWORK_CAPTURE_STOP; public static instance: NetworkCaptureStopTool | null = null; constructor() { super(); if (NetworkCaptureStopTool.instance) { return NetworkCaptureStopTool.instance; } NetworkCaptureStopTool.instance = this; } async execute(): Promise { console.log(`NetworkCaptureStopTool: Executing`); try { const startTool = NetworkCaptureStartTool.instance; if (!startTool) { return createErrorResponse('Network capture V2 start tool instance not found'); } // Get all tabs currently capturing const ongoingCaptures = Array.from(startTool.captureData.keys()); console.log( `NetworkCaptureStopTool: Found ${ongoingCaptures.length} ongoing captures: ${ongoingCaptures.join(', ')}`, ); if (ongoingCaptures.length === 0) { return createErrorResponse('No active network captures found in any tab.'); } // Get current active tab const activeTabs = await chrome.tabs.query({ active: true, currentWindow: true }); const activeTabId = activeTabs[0]?.id; // Determine the primary tab to stop let primaryTabId: number; if (activeTabId && startTool.captureData.has(activeTabId)) { // If current active tab is capturing, prioritize stopping it primaryTabId = activeTabId; console.log( `NetworkCaptureStopTool: Active tab ${activeTabId} is capturing, will stop it first.`, ); } else if (ongoingCaptures.length === 1) { // If only one tab is capturing, stop it primaryTabId = ongoingCaptures[0]; console.log( `NetworkCaptureStopTool: Only one tab ${primaryTabId} is capturing, stopping it.`, ); } else { // If multiple tabs are capturing but current active tab is not among them, stop the first one primaryTabId = ongoingCaptures[0]; console.log( `NetworkCaptureStopTool: Multiple tabs capturing, active tab not among them. Stopping tab ${primaryTabId} first.`, ); } const stopResult = await startTool.stopCapture(primaryTabId); if (!stopResult.success) { return createErrorResponse( stopResult.message || `Failed to stop network capture for tab ${primaryTabId}`, ); } // If multiple tabs are capturing, stop other tabs if (ongoingCaptures.length > 1) { const otherTabIds = ongoingCaptures.filter((id) => id !== primaryTabId); console.log( `NetworkCaptureStopTool: Stopping ${otherTabIds.length} additional captures: ${otherTabIds.join(', ')}`, ); for (const tabId of otherTabIds) { try { await startTool.stopCapture(tabId); } catch (error) { console.error(`NetworkCaptureStopTool: Error stopping capture on tab ${tabId}:`, error); } } } return { content: [ { type: 'text', text: JSON.stringify({ success: true, message: `Capture complete. ${stopResult.data?.requestCount || 0} requests captured.`, tabId: primaryTabId, tabUrl: stopResult.data?.tabUrl || 'N/A', tabTitle: stopResult.data?.tabTitle || 'Unknown Tab', requestCount: stopResult.data?.requestCount || 0, commonRequestHeaders: stopResult.data?.commonRequestHeaders || {}, commonResponseHeaders: stopResult.data?.commonResponseHeaders || {}, requests: stopResult.data?.requests || [], captureStartTime: stopResult.data?.captureStartTime, captureEndTime: stopResult.data?.captureEndTime, totalDurationMs: stopResult.data?.totalDurationMs, settingsUsed: stopResult.data?.settingsUsed || {}, totalRequestsReceived: stopResult.data?.totalRequestsReceived || 0, requestLimitReached: stopResult.data?.requestLimitReached || false, remainingCaptures: Array.from(startTool.captureData.keys()), }), }, ], isError: false, }; } catch (error: any) { console.error('NetworkCaptureStopTool: Critical error:', error); return createErrorResponse( `Error in NetworkCaptureStopTool: ${error.message || String(error)}`, ); } } } export const networkCaptureStartTool = new NetworkCaptureStartTool(); export const networkCaptureStopTool = new NetworkCaptureStopTool();