@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
JavaScript
/**
* 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