Skip to main content
Glama
network-intelligence-tools.ts18.9 kB
/** * Network Intelligence Tools for WebSee MCP Server * * Provides detailed network request analysis, timing, headers, and source tracing * for debugging and performance optimization. * * @module network-intelligence-tools */ import { z } from 'zod'; import { Page } from 'playwright'; import { SourceIntelligenceLayer } from '../index.js'; // ==================== Zod Schemas ==================== export const NetworkGetRequestsSchema = z.object({ url: z.string().url().describe('The page URL to analyze'), waitTime: z .number() .optional() .default(3000) .describe('Time to wait for requests to complete (ms)'), }); export const NetworkGetByUrlSchema = z.object({ url: z.string().url().describe('The page URL'), pattern: z.string().describe("URL pattern to filter (e.g., '/api/*', '*.json')"), }); export const NetworkGetTimingSchema = z.object({ url: z.string().url().describe('The page URL'), requestUrl: z.string().describe('The specific request URL to get timing for'), }); export const NetworkTraceInitiatorSchema = z.object({ url: z.string().url().describe('The page URL'), requestUrl: z.string().describe('The specific request URL to trace'), }); export const NetworkGetHeadersSchema = z.object({ url: z.string().url().describe('The page URL'), requestUrl: z.string().describe('The specific request URL to get headers for'), }); export const NetworkGetBodySchema = z.object({ url: z.string().url().describe('The page URL'), requestUrl: z.string().describe('The specific request URL to get body for'), }); // ==================== Type Definitions ==================== export interface NetworkRequest { url: string; method: string; status?: number; duration?: number; size?: number; timestamp: number; initiator?: { type: 'fetch' | 'xhr' | 'script' | 'parser' | 'other'; lineNumber?: number; columnNumber?: number; url?: string; }; stackTrace?: string[]; requestHeaders?: Record<string, string>; responseHeaders?: Record<string, string>; } export interface NetworkTiming { dns: number; connect: number; ssl: number; ttfb: number; download: number; total: number; } export interface InitiatorTrace { file: string; line: number; column: number; function?: string; stackTrace: Array<{ file: string; line: number; column: number; function?: string; }>; } export interface NetworkHeaders { requestHeaders: Record<string, string>; responseHeaders: Record<string, string>; } export interface NetworkBody { requestBody: string | null; responseBody: string | null; contentType: string; } // ==================== Helper Functions ==================== // Global storage for page-level network data const pageNetworkData = new WeakMap<Page, { intelligence: SourceIntelligenceLayer; requestMap: Map<string, any>; responseMap: Map<string, any>; listenersInstalled: boolean; }>(); /** * Get or create network tracking data for a page */ async function getOrCreateNetworkTracking(page: Page): Promise<{ intelligence: SourceIntelligenceLayer; requestMap: Map<string, any>; responseMap: Map<string, any>; }> { let data = pageNetworkData.get(page); if (!data) { // Create new tracking data for this page const intelligence = new SourceIntelligenceLayer(); const requestMap = new Map<string, any>(); const responseMap = new Map<string, any>(); // Set up Playwright event listeners FIRST (these capture ALL requests) page.on('request', request => { requestMap.set(request.url(), { url: request.url(), method: request.method(), headers: request.headers(), postData: request.postData(), resourceType: request.resourceType(), timestamp: Date.now(), }); }); page.on('response', async response => { const request = response.request(); const timing = request.timing(); // Capture response body let body: string | null = null; try { const buffer = await response.body(); body = buffer.toString('utf-8'); } catch (error) { // Response body might not be available for some requests (redirects, etc.) body = null; } responseMap.set(response.url(), { url: response.url(), status: response.status(), statusText: response.statusText(), headers: response.headers(), timing: timing, body: body, }); }); // Initialize intelligence layer (sets up network tracer) await intelligence.initialize(page); data = { intelligence, requestMap, responseMap, listenersInstalled: true }; pageNetworkData.set(page, data); } return data; } /** * Enhanced page initialization with network tracking */ async function initializePageWithNetworkTracking( page: Page, url: string, waitTime: number = 3000 ): Promise<{ intelligence: SourceIntelligenceLayer; requestMap: Map<string, any>; responseMap: Map<string, any>; }> { // Get or create tracking (this ensures we capture requests even if tool is called late) const { intelligence, requestMap, responseMap } = await getOrCreateNetworkTracking(page); // Only navigate if we're not already at the target URL // This prevents double-navigations and capturing unwanted requests const currentUrl = page.url(); if (currentUrl !== url) { // Clear existing data before navigation to avoid pollution requestMap.clear(); responseMap.clear(); intelligence.clearNetworkTraces(); await page.goto(url, { waitUntil: 'networkidle' }); } await page.waitForTimeout(waitTime); return { intelligence, requestMap, responseMap }; } /** * Clear network tracking data for a page (useful for tests) */ export function clearNetworkData(page: Page): void { const data = pageNetworkData.get(page); if (data) { data.requestMap.clear(); data.responseMap.clear(); data.intelligence.clearNetworkTraces(); } } /** * Match request URL using pattern (supports wildcards) */ function matchesPattern(url: string, pattern: string): boolean { const regex = new RegExp('^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + '$'); return regex.test(url); } /** * Calculate detailed timing metrics from Playwright timing object */ function calculateTiming(timing: any): NetworkTiming { return { dns: timing.domainLookupEnd - timing.domainLookupStart, connect: timing.connectEnd - timing.connectStart, ssl: timing.secureConnectionStart > 0 ? timing.connectEnd - timing.secureConnectionStart : 0, ttfb: timing.responseStart - timing.requestStart, download: timing.responseEnd - timing.responseStart, total: timing.responseEnd - timing.requestStart, }; } /** * Parse stack trace to extract source locations */ function parseStackTrace(stackTrace: string[]): InitiatorTrace['stackTrace'] { return stackTrace .map(line => { const match = line.match(/at\s+(?:(.+?)\s+)?\(?(.+?):(\d+):(\d+)\)?/); if (match) { const [, func, file, lineStr, colStr] = match; return { function: func?.trim() || 'anonymous', file: file.trim(), line: parseInt(lineStr), column: parseInt(colStr), }; } return null; }) .filter((item): item is NonNullable<typeof item> => item !== null); } // ==================== Tool Implementations ==================== /** * Get all network requests for a page */ export async function networkGetRequests( page: Page, params: z.infer<typeof NetworkGetRequestsSchema> ): Promise<{ requests: NetworkRequest[] }> { const { intelligence, requestMap, responseMap } = await initializePageWithNetworkTracking( page, params.url, params.waitTime ); const traces = intelligence.getNetworkTraces(); const requests: NetworkRequest[] = []; const processedUrls = new Set<string>(); // Helper to normalize URLs for comparison (resolve relative to absolute) const normalizeUrl = (url: string): string => { try { // If it's already absolute, return as-is if (url.startsWith('http://') || url.startsWith('https://')) { return url; } // If it's relative, try to resolve it against page URL const pageUrl = page.url(); if (pageUrl && pageUrl !== 'about:blank') { return new URL(url, pageUrl).href; } return url; } catch { return url; } }; // First, add all traces from intelligence layer (these have stack traces) for (const trace of traces) { const normalizedTraceUrl = normalizeUrl(trace.url); // Try to find matching request/response data by both original and normalized URL const requestData = requestMap.get(trace.url) || requestMap.get(normalizedTraceUrl); const responseData = responseMap.get(trace.url) || responseMap.get(normalizedTraceUrl); requests.push({ url: trace.url, // Keep the original URL format from the trace method: trace.method, status: trace.status || responseData?.status, duration: trace.duration, size: responseData?.headers?.['content-length'] ? parseInt(responseData.headers['content-length']) : undefined, timestamp: trace.timestamp, initiator: trace.initiator, stackTrace: trace.stackTrace, requestHeaders: requestData?.headers, responseHeaders: responseData?.headers, }); // Mark both URLs as processed to avoid duplicates processedUrls.add(trace.url); processedUrls.add(normalizedTraceUrl); } // Then, add requests from Playwright that weren't captured by intelligence layer // This handles cases where the page was loaded before network tracing was initialized for (const [url, requestData] of requestMap.entries()) { if (!processedUrls.has(url)) { const responseData = responseMap.get(url); requests.push({ url: url, method: requestData.method, status: responseData?.status, duration: responseData?.timing ? responseData.timing.responseEnd - responseData.timing.requestStart : undefined, size: responseData?.headers?.['content-length'] ? parseInt(responseData.headers['content-length']) : undefined, timestamp: requestData.timestamp, requestHeaders: requestData.headers, responseHeaders: responseData?.headers, }); processedUrls.add(url); } } return { requests }; } /** * Filter network requests by URL pattern */ export async function networkGetByUrl( page: Page, params: z.infer<typeof NetworkGetByUrlSchema> ): Promise<{ requests: NetworkRequest[] }> { const { requests } = await networkGetRequests(page, { url: params.url, waitTime: 3000, }); const filteredRequests = requests.filter(req => matchesPattern(req.url, params.pattern)); return { requests: filteredRequests.map(req => ({ url: req.url, method: req.method, status: req.status, duration: req.duration, timestamp: req.timestamp, initiator: req.initiator, })), }; } /** * Get detailed timing information for a specific request */ export async function networkGetTiming( page: Page, params: z.infer<typeof NetworkGetTimingSchema> ): Promise<NetworkTiming | { error: string }> { // Use existing tracking data instead of creating new listeners const { responseMap } = await getOrCreateNetworkTracking(page); // Wait briefly for any pending responses await page.waitForTimeout(500); const responseData = responseMap.get(params.requestUrl); if (!responseData || !responseData.timing) { return { error: `Request not found: ${params.requestUrl}`, }; } return calculateTiming(responseData.timing); } /** * Trace network request to its source code origin */ export async function networkTraceInitiator( page: Page, params: z.infer<typeof NetworkTraceInitiatorSchema> ): Promise<InitiatorTrace | { error: string }> { // Use existing intelligence layer const { intelligence } = await getOrCreateNetworkTracking(page); // Wait briefly for any pending traces await page.waitForTimeout(500); const traces = intelligence.getNetworkTraces(); const targetTrace = traces.find(t => t.url === params.requestUrl); if (!targetTrace) { return { error: `Request not found: ${params.requestUrl}`, }; } if (!targetTrace.stackTrace || targetTrace.stackTrace.length === 0) { return { error: 'No stack trace available for this request', }; } // Parse the first stack frame as the immediate initiator const parsedStack = parseStackTrace(targetTrace.stackTrace); if (parsedStack.length === 0) { return { error: 'Unable to parse stack trace', }; } const initiator = parsedStack[0]; return { file: initiator.file, line: initiator.line, column: initiator.column, function: initiator.function, stackTrace: parsedStack, }; } /** * Get request and response headers for a specific request */ export async function networkGetHeaders( page: Page, params: z.infer<typeof NetworkGetHeadersSchema> ): Promise<NetworkHeaders | { error: string }> { // Use existing tracking data const { requestMap, responseMap } = await getOrCreateNetworkTracking(page); // Wait briefly for any pending data await page.waitForTimeout(500); const requestData = requestMap.get(params.requestUrl); const responseData = responseMap.get(params.requestUrl); if (!requestData && !responseData) { return { error: `Request not found: ${params.requestUrl}`, }; } return { requestHeaders: requestData?.headers || {}, responseHeaders: responseData?.headers || {}, }; } /** * Get request and response body for a specific request */ export async function networkGetBody( page: Page, params: z.infer<typeof NetworkGetBodySchema> ): Promise<NetworkBody | { error: string }> { // Use existing tracking data const { requestMap, responseMap } = await getOrCreateNetworkTracking(page); // Wait briefly for any pending data await page.waitForTimeout(500); const requestData = requestMap.get(params.requestUrl); const responseData = responseMap.get(params.requestUrl); if (!requestData && !responseData) { return { error: `Request not found: ${params.requestUrl}`, }; } const contentType = responseData?.headers?.['content-type'] || ''; return { requestBody: requestData?.postData || null, responseBody: responseData?.body || null, contentType: contentType, }; } // ==================== Tool Definitions for MCP ==================== /** * Tool definitions compatible with MCP server */ export const networkIntelligenceTools = [ { name: 'network_get_requests', description: 'Get all network requests made by a page with detailed information', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'The page URL to analyze', }, waitTime: { type: 'number', description: 'Time to wait for requests to complete (ms)', default: 3000, }, }, required: ['url'], }, }, { name: 'network_get_by_url', description: 'Filter network requests by URL pattern (supports wildcards)', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'The page URL', }, pattern: { type: 'string', description: "URL pattern to filter (e.g., '/api/*', '*.json')", }, }, required: ['url', 'pattern'], }, }, { name: 'network_get_timing', description: 'Get detailed timing metrics for a specific network request', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'The page URL', }, requestUrl: { type: 'string', description: 'The specific request URL to get timing for', }, }, required: ['url', 'requestUrl'], }, }, { name: 'network_trace_initiator', description: 'Trace a network request to its source code origin', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'The page URL', }, requestUrl: { type: 'string', description: 'The specific request URL to trace', }, }, required: ['url', 'requestUrl'], }, }, { name: 'network_get_headers', description: 'Get request and response headers for a specific network request', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'The page URL', }, requestUrl: { type: 'string', description: 'The specific request URL to get headers for', }, }, required: ['url', 'requestUrl'], }, }, { name: 'network_get_body', description: 'Get request and response body for a specific network request', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'The page URL', }, requestUrl: { type: 'string', description: 'The specific request URL to get body for', }, }, required: ['url', 'requestUrl'], }, }, ]; // ==================== Tool Handler Factory ==================== /** * Create a tool handler for the MCP server */ export function createNetworkToolHandler(page: Page) { return async (toolName: string, args: any): Promise<any> => { switch (toolName) { case 'network_get_requests': { const params = NetworkGetRequestsSchema.parse(args); return await networkGetRequests(page, params); } case 'network_get_by_url': { const params = NetworkGetByUrlSchema.parse(args); return await networkGetByUrl(page, params); } case 'network_get_timing': { const params = NetworkGetTimingSchema.parse(args); return await networkGetTiming(page, params); } case 'network_trace_initiator': { const params = NetworkTraceInitiatorSchema.parse(args); return await networkTraceInitiator(page, params); } case 'network_get_headers': { const params = NetworkGetHeadersSchema.parse(args); return await networkGetHeaders(page, params); } case 'network_get_body': { const params = NetworkGetBodySchema.parse(args); return await networkGetBody(page, params); } default: throw new Error(`Unknown network tool: ${toolName}`); } }; } // ==================== Exports ==================== // All exports are already defined above with their declarations

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/1AQuantum/websee-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server