UNPKG

@grebyn/toolflow-mcp-server

Version:

MCP server for managing other MCP servers - discover, install, organize into bundles, and automate with workflows. Uses StreamableHTTP transport with dual OAuth/API key authentication.

157 lines 4.81 kB
/** * Shared Logging Utilities * * Centralized utilities for MCP logging to eliminate code duplication * and provide consistent sanitization and authentication handling */ /** * Sanitize sensitive data from any object * Enhanced to handle nested structures and more token patterns */ export function sanitizeData(data) { if (!data) return data; try { // Create a deep copy to avoid mutating original data const sanitized = JSON.parse(JSON.stringify(data)); // Sensitive field names to redact const sensitiveFields = [ 'password', 'token', 'secret', 'api_key', 'apiKey', 'authorization', 'auth', 'credential', 'private_key', 'access_token', 'refresh_token', 'client_secret', 'jwt', 'bearer', 'cookie', 'session', 'key' ]; // Recursively sanitize object function sanitizeRecursive(obj) { if (typeof obj !== 'object' || obj === null) { // Check if string looks like a token if (typeof obj === 'string' && obj.length > 10) { if (obj.match(/^(Bearer |sk[-_]|pk[-_]|tfl_|ghp_|ghs_|npm_|eyJ|[A-Za-z0-9+/]{20,}={0,2}$)/)) { return '[REDACTED]'; } } return obj; } // Handle arrays if (Array.isArray(obj)) { return obj.map(item => sanitizeRecursive(item)); } // Handle objects for (const key in obj) { const lowerKey = key.toLowerCase(); // Check if field name contains sensitive keywords if (sensitiveFields.some(field => lowerKey.includes(field))) { obj[key] = '[REDACTED]'; } else { obj[key] = sanitizeRecursive(obj[key]); } } return obj; } return sanitizeRecursive(sanitized); } catch (error) { console.error('Error sanitizing data:', error); return { sanitization_error: 'Failed to sanitize data' }; } } /** * Calculate approximate size of JSON data in bytes */ export function calculateDataSize(data) { try { return JSON.stringify(data).length; } catch { return 0; } } /** * Extract client identifier from user agent string */ export function extractClientIdentifier(userAgent) { if (!userAgent) return undefined; const patterns = [ { pattern: /Cursor\/(\S+)/, name: 'Cursor' }, { pattern: /Claude-Desktop\/(\S+)/, name: 'Claude Desktop' }, { pattern: /VSCode\/(\S+)/, name: 'VS Code' }, { pattern: /JetBrains\/(\S+)/, name: 'JetBrains' }, { pattern: /Windsurf\/(\S+)/, name: 'Windsurf' }, { pattern: /Zed\/(\S+)/, name: 'Zed' } ]; for (const { pattern, name } of patterns) { const match = userAgent.match(pattern); if (match) { return `${name} ${match[1]}`; } } if (userAgent.includes('MCP')) { return 'Generic MCP Client'; } return undefined; } /** * Validate authentication from user context * Returns auth header or throws error */ export function getAuthHeader(context) { if (context.apiKey) { return `Bearer ${context.apiKey}`; } if (context.token) { return `Bearer ${context.token}`; } throw new Error('No authentication available for logging'); } /** * Determine execution status from result/error */ export function determineExecutionStatus(error) { if (!error) return 'success'; // If it's an Error object, it's a system error if (error instanceof Error) return 'error'; // Otherwise it's a business logic failure return 'failure'; } /** * Extract error message safely */ export function extractErrorMessage(error) { if (!error) return undefined; if (error instanceof Error) return error.message; if (typeof error === 'string') return error; try { return JSON.stringify(error); } catch { return 'Unknown error'; } } /** * Basic retry wrapper for database operations */ export async function withRetry(operation, maxRetries = 1) { let lastError; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await operation(); } catch (error) { lastError = error; // Don't retry on the last attempt if (attempt === maxRetries) break; // Simple delay before retry await new Promise(resolve => setTimeout(resolve, 100)); } } throw lastError; } //# sourceMappingURL=shared-logging.js.map