Skip to main content
Glama
json-serializer.tsβ€’11.2 kB
/** * Safe JSON serialization utilities to prevent MCP protocol breakdown * Handles circular references, non-serializable values, and large objects * * Uses fast-safe-stringify for improved performance and reliability * * IMPORTANT MCP PROTOCOL WARNING: * Never use console.log() in this file or any MCP-related code. * Always route diagnostics through the structured logger or safe MCP logging helpers. * Using console.log will break the MCP protocol, as it writes to stdout * which is used for client-server communication. */ // Support both CJS and ESM default export shapes for fast-safe-stringify import * as fastSafeStringifyNs from 'fast-safe-stringify'; type ScopedLogger = { warn: (message: string, data?: Record<string, unknown>) => void; error: ( message: string, errorObj?: unknown, data?: Record<string, unknown> ) => void; }; let serializerLoggerPromise: Promise<ScopedLogger> | null = null; let safeCopyLoggerPromise: Promise<ScopedLogger> | null = null; const noopLogger: ScopedLogger = { warn: () => { // intentionally noop when logging is unavailable }, error: () => { // intentionally noop when logging is unavailable }, }; function getSerializationLogger( operation: 'safeJsonStringify' | 'createSafeCopy' ): Promise<ScopedLogger> { if (operation === 'safeJsonStringify') { if (!serializerLoggerPromise) { serializerLoggerPromise = import('./logger.js') .then( (module) => module.createScopedLogger( 'utils.json-serializer', 'safeJsonStringify', module.OperationType.SYSTEM ) as ScopedLogger ) .catch(() => noopLogger); } return serializerLoggerPromise; } if (!safeCopyLoggerPromise) { safeCopyLoggerPromise = import('./logger.js') .then( (module) => module.createScopedLogger( 'utils.json-serializer', 'createSafeCopy', module.OperationType.SYSTEM ) as ScopedLogger ) .catch(() => noopLogger); } return safeCopyLoggerPromise; } type FastSafeStringifyFn = ( value: unknown, replacer?: (key: string, value: unknown) => unknown, space?: string | number ) => string; const fastSafeStringify: FastSafeStringifyFn = ((fastSafeStringifyNs as Record<string, unknown>) .default as FastSafeStringifyFn) || (fastSafeStringifyNs as unknown as FastSafeStringifyFn); /** * Interface for serialization options */ export interface SerializationOptions { /** Maximum depth for nested objects (only used in the legacy implementation) */ maxDepth?: number; /** Maximum string length before truncation */ maxStringLength?: number; /** Whether to include stack traces in error objects */ includeStackTraces?: boolean; /** Custom replacer function */ replacer?: (key: string, value: unknown) => unknown; /** Indent spaces for pretty printing (default: 2) */ indent?: number; } /** * Default serialization options */ const DEFAULT_OPTIONS: Required<SerializationOptions> = { maxDepth: 20, // Kept for backward compatibility maxStringLength: 25000, // 25KB max string length - more reasonable for MCP includeStackTraces: false, replacer: (key: string, value: unknown) => value, indent: 2, }; /** * Safe JSON stringify that handles circular references and non-serializable values * * Uses fast-safe-stringify for high performance and reliability * * @param obj - The object to stringify * @param options - Serialization options * @returns Safe JSON string */ export function safeJsonStringify( obj: unknown, options: SerializationOptions = {} ): string { const opts = { ...DEFAULT_OPTIONS, ...options }; // Performance monitoring for large objects const startTime = performance.now(); try { // Create a custom replacer to handle non-standard values const customReplacer = (key: string, value: unknown): unknown => { // First apply user-provided replacer if any value = opts.replacer(key, value); // Handle undefined (normally skipped by JSON) if (value === undefined) { return null; } // Handle very long strings if (typeof value === 'string' && value.length > opts.maxStringLength) { return value.substring(0, opts.maxStringLength) + '... [truncated]'; } // Handle special object types more gracefully if (value instanceof Error) { const errorObj: Record<string, unknown> = { name: value.name, message: value.message, }; if (opts.includeStackTraces && value.stack) { errorObj.stack = value.stack; } if ('cause' in value && value.cause) { errorObj.cause = value.cause; } return errorObj; } if (value instanceof Map) { return '[Map: ' + value.size + ' entries]'; } if (value instanceof Set) { return '[Set: ' + value.size + ' items]'; } if (ArrayBuffer && value instanceof ArrayBuffer) { return '[ArrayBuffer: ' + value.byteLength + ' bytes]'; } return value; }; // Use fast-safe-stringify with our custom replacer const result = fastSafeStringify(obj, customReplacer, opts.indent); // Performance monitoring and logging const duration = performance.now() - startTime; if (duration > 100) { const durationMs = Math.round(duration * 100) / 100; void getSerializationLogger('safeJsonStringify') .then((logger) => logger.warn( `Slow serialization detected: ${durationMs}ms for ${typeof obj}`, { durationMs, serializedLength: result.length, } ) ) .catch(() => { // Logger unavailable; nothing else to do in non-critical path }); } return result; } catch (error: unknown) { // Enhanced error context const duration = performance.now() - startTime; const durationMs = Math.round(duration * 100) / 100; void getSerializationLogger('safeJsonStringify') .then((logger) => logger.error( `Serialization failed after ${durationMs}ms for ${typeof obj}`, error, { durationMs, valueType: typeof obj, } ) ) .catch(() => { // Logger unavailable; nothing else to do in non-critical path }); // Use fast-safe-stringify directly for the error fallback return fastSafeStringify( { error: 'Serialization failed', message: error instanceof Error ? error.message : String(error), originalType: typeof obj, timestamp: new Date().toISOString(), duration: `${duration.toFixed(2)}ms`, }, undefined, 2 ); } } /** * Validates that a JSON string is properly formed and can be parsed * * @param jsonString - The JSON string to validate * @returns Object with validation result and parsed data if successful */ export function validateJsonString(jsonString: string): { isValid: boolean; data?: unknown; error?: string; size: number; } { const size = Buffer.byteLength(jsonString, 'utf8'); try { const data = JSON.parse(jsonString); return { isValid: true, data, size, }; } catch (error: unknown) { return { isValid: false, error: error instanceof Error ? error.message : String(error), size, }; } } /** * Detects potential circular references in an object before serialization * * NOTE: This is less critical now with fast-safe-stringify, but kept for * compatibility with existing code that uses it. * * @param obj - The object to check * @param maxDepth - Maximum depth to check * @returns True if circular references are detected */ export function hasCircularReferences( obj: unknown, maxDepth: number = 10 ): boolean { const seen = new WeakSet(); function check(value: unknown, depth: number): boolean { if (depth > maxDepth) return false; if (value === null || typeof value !== 'object') return false; if (seen.has(value)) return true; seen.add(value); try { if (Array.isArray(value)) { return value.some((item) => check(item, depth + 1)); } else { return Object.values(value).some((val) => check(val, depth + 1)); } } finally { seen.delete(value); } } return check(obj, 0); } /** * Creates a safe copy of an object that can be JSON serialized * * Uses fast-safe-stringify for improved performance and reliability * * @param obj - The object to copy * @param options - Serialization options * @returns Safe copy of the object */ export function createSafeCopy( obj: unknown, options: SerializationOptions = {} ): unknown { try { // Fast path: directly use fast-safe-stringify to create a JSON string const jsonString = safeJsonStringify(obj, options); // Parse it back to create the safe copy return JSON.parse(jsonString); } catch (error: unknown) { void getSerializationLogger('createSafeCopy') .then((logger) => logger.error('Failed to create safe copy', error, { message: error instanceof Error ? error.message : String(error), originalType: typeof obj, }) ) .catch(() => { // Logger unavailable; nothing else to do in non-critical path }); // Return a structured error object return { error: 'Failed to create safe copy', message: error instanceof Error ? error.message : String(error), originalType: typeof obj, }; } } /** * Sanitizes MCP response objects to prevent JSON parsing errors * * Uses fast-safe-stringify to ensure all responses are safely serializable * * @param response - The MCP response object to sanitize * @returns Sanitized response object */ export function sanitizeMcpResponse(response: unknown): unknown { // Ensure response has the correct structure if (!response || typeof response !== 'object') { return { content: [ { type: 'text', text: 'Invalid response structure', }, ], isError: true, error: { code: 500, message: 'Response sanitization failed', type: 'sanitization_error', }, }; } try { // Create safe copy with MCP-specific options optimized for Attio responses return createSafeCopy(response, { maxStringLength: 40000, // 40KB for response content - reasonable limit includeStackTraces: false, }); } catch (error: unknown) { // Provide a valid fallback response if sanitization fails return { content: [ { type: 'text', text: 'Error processing response. The server encountered an error while formatting the response data.', }, ], isError: true, error: { code: 500, message: 'Response sanitization failed: ' + (error instanceof Error ? error.message : String(error)), type: 'sanitization_error', }, }; } }

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/kesslerio/attio-mcp-server'

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