Skip to main content
Glama

Web Proxy MCP Server

by mako10k
traffic-analyzer.js12.1 kB
/** * Traffic Analyzer * Captures, stores, and analyzes proxy traffic */ import fs from 'fs/promises'; import path from 'path'; import assert from 'assert'; export class TrafficAnalyzer { constructor(options = {}) { // Pre-conditions assert(typeof options === 'object', 'options must be an object'); this.entries = []; this.maxEntries = options.maxEntries || 10000; this.enablePersistence = options.enablePersistence || false; this.persistenceFile = options.persistenceFile || './traffic-log.json'; this.stats = { totalRequests: 0, totalResponseTime: 0, domainsTracked: new Set(), startTime: new Date() }; // Post-conditions assert(Array.isArray(this.entries), 'entries must be an array'); assert(typeof this.maxEntries === 'number' && this.maxEntries > 0, 'maxEntries must be a positive number'); } /** * Add traffic entry * @param {Object} entry - Traffic entry */ addEntry(entry) { // Pre-conditions assert(entry, 'entry is required'); assert(typeof entry === 'object', 'entry must be an object'); assert(typeof entry.timestamp === 'string', 'entry.timestamp must be a string'); assert(typeof entry.url === 'string', 'entry.url must be a string'); assert(typeof entry.method === 'string', 'entry.method must be a string'); // Ensure required fields const completeEntry = { id: this._generateId(), timestamp: entry.timestamp || new Date().toISOString(), url: entry.url, method: entry.method || 'GET', domain: entry.domain, statusCode: entry.statusCode, responseTime: entry.responseTime || 0, headers: entry.headers, tunneled: entry.tunneled || false, bodyCaptured: entry.bodyCaptured || false, note: entry.note }; this.entries.push(completeEntry); // Update stats this.stats.totalRequests++; this.stats.totalResponseTime += completeEntry.responseTime; this.stats.domainsTracked.add(completeEntry.domain); // Trim entries if exceeding limit if (this.entries.length > this.maxEntries) { this.entries.shift(); } // Persist if enabled if (this.enablePersistence) { this._persistEntries().catch(console.error); } // Post-conditions assert(this.entries.length > 0, 'Entry should have been added to entries array'); assert(this.entries[this.entries.length - 1].id === completeEntry.id, 'Last entry should match the added entry'); assert(this.stats.totalRequests > 0, 'Total requests should have been incremented'); console.log(`✅ Traffic entry added: ${completeEntry.id} (${completeEntry.method} ${completeEntry.url})`); return completeEntry.id; } /** * Get entries with filtering * @param {Object} options - Filter options * @returns {Array} Filtered entries */ getEntries(options = {}) { let filtered = [...this.entries]; // Filter by domain if (options.domain) { filtered = filtered.filter(entry => entry.domain.includes(options.domain) ); } // Filter by method if (options.method) { filtered = filtered.filter(entry => entry.method === options.method.toUpperCase() ); } // Filter by time if (options.since) { const sinceTime = new Date(options.since).getTime(); filtered = filtered.filter(entry => new Date(entry.timestamp).getTime() >= sinceTime ); } // Sort by timestamp (newest first) filtered.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime() ); // Limit results if (options.limit) { filtered = filtered.slice(0, options.limit); } return filtered; } /** * Get all entries * @returns {Array} All entries */ getAllEntries() { return [...this.entries]; } /** * Get entry count * @returns {number} Number of entries */ getEntryCount() { return this.entries.length; } /** * Clear entries * @param {string} domain - Optional domain filter * @returns {number} Number of cleared entries */ clearEntries(domain = null) { let cleared = 0; if (domain) { const originalLength = this.entries.length; this.entries = this.entries.filter(entry => !entry.domain.includes(domain) ); cleared = originalLength - this.entries.length; } else { cleared = this.entries.length; this.entries = []; this.stats = { totalRequests: 0, totalResponseTime: 0, domainsTracked: new Set(), startTime: new Date() }; } return cleared; } /** * Analyze traffic patterns * @param {Object} options - Analysis options * @returns {Object} Analysis results */ analyzeTraffic(options = {}) { const entries = this.getEntries({ domain: options.domain, since: this._getTimeframeSince(options.timeframe || '24h') }); if (entries.length === 0) { return { summary: { totalRequests: 0, uniqueDomains: 0, timeframe: options.timeframe || '24h' } }; } const analysis = { summary: { totalRequests: entries.length, uniqueDomains: new Set(entries.map(e => e.domain)).size, timeframe: options.timeframe || '24h', avgResponseTime: this._calculateAverage(entries.map(e => e.responseTime)), errorRate: entries.filter(e => e.statusCode >= 400).length / entries.length }, grouped: this._groupEntries(entries, options.groupBy || 'domain') }; return analysis; } /** * Export traffic as HAR format * @param {Object} options - Export options * @returns {Object} Export info */ async exportHAR(options = {}) { const entries = this.getEntries({ domain: options.domain, since: options.since }); const har = { log: { version: "1.2", creator: { name: "Web Proxy MCP Server", version: "1.0" }, pages: [], entries: entries.map(entry => this._convertToHAREntry(entry)) } }; const filename = options.filename ? `${options.filename}.har` : `traffic-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.har`; const filepath = path.resolve(filename); const content = JSON.stringify(har, null, 2); await fs.writeFile(filepath, content); return { filename, filepath, entryCount: entries.length, fileSize: content.length }; } /** * Get traffic statistics * @returns {Object} Traffic statistics */ getStats() { return { totalEntries: this.entries.length, totalRequests: this.stats.totalRequests, uniqueDomains: this.stats.domainsTracked.size, averageResponseTime: this.stats.totalRequests > 0 ? this.stats.totalResponseTime / this.stats.totalRequests : 0, uptimeSeconds: Math.floor((Date.now() - this.stats.startTime.getTime()) / 1000), recentActivity: this._getRecentActivity() }; } /** * Load persisted entries */ async loadPersistedEntries() { if (!this.enablePersistence) return; try { const data = await fs.readFile(this.persistenceFile, 'utf-8'); const entries = JSON.parse(data); if (Array.isArray(entries)) { this.entries = entries.slice(-this.maxEntries); console.log(`Loaded ${this.entries.length} persisted traffic entries`); } } catch (error) { // File might not exist or be corrupted, start fresh console.log('No persisted traffic entries found, starting fresh'); } } /** * Generate unique ID for entry * @private */ _generateId() { return `entry-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } /** * Persist entries to file * @private */ async _persistEntries() { try { await fs.writeFile( this.persistenceFile, JSON.stringify(this.entries, null, 2) ); } catch (error) { console.error('Failed to persist traffic entries:', error.message); } } /** * Convert timestamp to Date for timeframe filtering * @private */ _getTimeframeSince(timeframe) { const now = new Date(); const hours = { '1h': 1, '24h': 24, '7d': 24 * 7, '30d': 24 * 30 }; const hoursBack = hours[timeframe] || 24; return new Date(now.getTime() - hoursBack * 60 * 60 * 1000); } /** * Group entries by specified field * @private */ _groupEntries(entries, groupBy) { const groups = {}; entries.forEach(entry => { let key; switch (groupBy) { case 'domain': key = entry.domain; break; case 'method': key = entry.method; break; case 'status': key = Math.floor(entry.statusCode / 100) * 100; // Group by status class break; case 'hour': key = new Date(entry.timestamp).getHours(); break; default: key = 'unknown'; } if (!groups[key]) { groups[key] = { count: 0, totalResponseTime: 0, statusCodes: {}, methods: {} }; } groups[key].count++; groups[key].totalResponseTime += entry.responseTime; groups[key].statusCodes[entry.statusCode] = (groups[key].statusCodes[entry.statusCode] || 0) + 1; groups[key].methods[entry.method] = (groups[key].methods[entry.method] || 0) + 1; }); // Calculate averages Object.values(groups).forEach(group => { group.avgResponseTime = group.totalResponseTime / group.count; }); return groups; } /** * Calculate average of array * @private */ _calculateAverage(numbers) { if (numbers.length === 0) return 0; return numbers.reduce((sum, num) => sum + num, 0) / numbers.length; } /** * Convert entry to HAR format * @private */ _convertToHAREntry(entry) { return { startedDateTime: entry.timestamp, time: entry.responseTime, request: { method: entry.method, url: entry.url, httpVersion: "HTTP/1.1", headers: this._formatHeaders(entry.headers?.request || {}), queryString: [], postData: entry.bodyCaptured ? {} : undefined, headersSize: -1, bodySize: -1 }, response: { status: entry.statusCode, statusText: this._getStatusText(entry.statusCode), httpVersion: "HTTP/1.1", headers: this._formatHeaders(entry.headers?.response || {}), content: { size: -1, mimeType: entry.headers?.response?.['content-type'] || "text/html" }, headersSize: -1, bodySize: -1 }, cache: {}, timings: { send: 0, wait: entry.responseTime, receive: 0 } }; } /** * Format headers for HAR * @private */ _formatHeaders(headers) { return Object.entries(headers).map(([name, value]) => ({ name, value: String(value) })); } /** * Get HTTP status text * @private */ _getStatusText(statusCode) { const statusTexts = { 200: 'OK', 201: 'Created', 204: 'No Content', 301: 'Moved Permanently', 302: 'Found', 304: 'Not Modified', 400: 'Bad Request', 401: 'Unauthorized', 403: 'Forbidden', 404: 'Not Found', 500: 'Internal Server Error', 502: 'Bad Gateway', 503: 'Service Unavailable' }; return statusTexts[statusCode] || 'Unknown'; } /** * Get recent activity summary * @private */ _getRecentActivity() { const recentEntries = this.getEntries({ limit: 10 }); return { lastRequestTime: recentEntries.length > 0 ? recentEntries[0].timestamp : null, recentDomains: [...new Set(recentEntries.map(e => e.domain))], requestsLast10: recentEntries.length }; } }

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/mako10k/mcp-web-proxy'

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