Skip to main content
Glama

Git MCP Server

json-response-formatter.ts10.5 kB
/** * JSON response formatter utilities for LLM-optimized tool responses. * Provides structured JSON output with configurable verbosity levels. * * IMPORTANT: Verbosity levels control which FIELDS are included, not array truncation. * LLMs require complete context - never truncate arrays. If size is a concern, * omit entire arrays at lower verbosity levels rather than truncating them. */ import { config } from '@/config/index.js'; import { countTokens } from '@/utils/metrics/tokenCounter.js'; import type { ContentBlock } from '@modelcontextprotocol/sdk/types.js'; // ============================================================================ // Types // ============================================================================ export type VerbosityLevel = 'minimal' | 'standard' | 'full'; export type ResponseFormat = 'json' | 'markdown' | 'auto'; export interface JsonFormatterOptions<T = unknown> { verbosity?: VerbosityLevel; filter?: (data: T, level: VerbosityLevel) => Partial<T> | T; prettyPrint?: boolean; replacer?: (key: string, value: unknown) => unknown; debug?: boolean; memoize?: boolean; } // ============================================================================ // Internal Utilities // ============================================================================ class JsonFormatterError extends Error { constructor( message: string, public readonly cause?: unknown, ) { super(message); this.name = 'JsonFormatterError'; } } function detectCircularReference( obj: unknown, seen = new WeakSet<object>(), path = 'root', ): string | null { if (obj === null || typeof obj !== 'object') return null; if (seen.has(obj)) return path; seen.add(obj); if (Array.isArray(obj)) { for (let i = 0; i < obj.length; i++) { const result = detectCircularReference(obj[i], seen, `${path}[${i}]`); if (result) return result; } } else { for (const [key, value] of Object.entries(obj)) { const result = detectCircularReference(value, seen, `${path}.${key}`); if (result) return result; } } return null; } function defaultReplacer(_key: string, value: unknown): unknown { if (value instanceof Date) return value.toISOString(); if (typeof value === 'bigint') return value.toString(); if (value === undefined) return null; if (typeof value === 'function') return undefined; return value; } function safeJsonStringify( data: unknown, replacer?: (key: string, value: unknown) => unknown, indent?: number, ): string { const circularPath = detectCircularReference(data); if (circularPath) { throw new JsonFormatterError( `Circular reference detected at: ${circularPath}`, ); } const combinedReplacer = (key: string, value: unknown) => { const processed = defaultReplacer(key, value); return replacer ? replacer(key, processed) : processed; }; try { return JSON.stringify(data, combinedReplacer, indent); } catch (error) { throw new JsonFormatterError( 'Failed to serialize to JSON', error instanceof Error ? error : undefined, ); } } /** * Estimate token count for a string using the proper tokenCounter utility. * This is a wrapper that makes the async countTokens function usable in * synchronous contexts (debug logging). For production use, prefer the * async countTokens function directly. */ function estimateTokenCount(text: string): number { // Quick synchronous approximation for debug output only // For accurate counts, use countTokens() from @/utils/metrics/tokenCounter const normalized = text.replace(/\s+/g, ' ').trim(); return Math.ceil(normalized.length / 4); } // ============================================================================ // Core Formatter // ============================================================================ /** * Creates a JSON response formatter with optional verbosity filtering. * * @example * const formatter = createJsonFormatter({ * filter: (data, level) => level === 'minimal' * ? { success: data.success } * : data * }); */ export function createJsonFormatter<T>( options?: JsonFormatterOptions<T>, ): (result: T) => ContentBlock[] { const { verbosity = config.mcpResponseVerbosity, filter, prettyPrint = true, replacer, debug = false, memoize = false, } = options || {}; const cache = memoize ? new Map<string, string>() : null; return (result: T): ContentBlock[] => { const startTime = debug ? Date.now() : 0; try { const filteredResult = filter ? filter(result, verbosity) : result; // Generate cache key once (if caching enabled) let cacheKey: string | null = null; if (cache) { try { cacheKey = JSON.stringify({ result: filteredResult, verbosity }); const cached = cache.get(cacheKey); if (cached) { if (debug) console.log('[JsonFormatter] Cache hit'); return [{ type: 'text', text: cached }]; } } catch { // Cache key gen failed, continue without caching cacheKey = null; } } const jsonString = safeJsonStringify( filteredResult, replacer, prettyPrint ? 2 : 0, ); // Store in cache (if key was successfully generated) if (cache && cacheKey) { cache.set(cacheKey, jsonString); } if (debug) { const elapsed = Date.now() - startTime; console.log( `[JsonFormatter] ${elapsed}ms, ~${estimateTokenCount(jsonString)} tokens`, ); } return [{ type: 'text', text: jsonString }]; } catch (error) { const errorMessage = error instanceof JsonFormatterError ? error.message : 'Unknown formatting error'; if (debug) console.error('[JsonFormatter] Error:', error); return [ { type: 'text', text: JSON.stringify( { error: 'JSON_FORMATTING_ERROR', message: errorMessage, verbosity, }, null, prettyPrint ? 2 : 0, ), }, ]; } }; } // ============================================================================ // Helper Functions // ============================================================================ /** * Creates a filter that includes/excludes fields by verbosity level. * * @example * const filter = filterByVerbosity({ * minimal: ['success', 'id'], * standard: ['success', 'id', 'files'], * full: '*' * }); */ export function filterByVerbosity<T extends Record<string, unknown>>(config: { minimal: (keyof T)[] | '*'; standard: (keyof T)[] | '*'; full: (keyof T)[] | '*'; }): (data: T, level: VerbosityLevel) => Partial<T> | T { return (data: T, level: VerbosityLevel) => { const fields = config[level]; if (fields === '*') return data; const filtered: Partial<T> = {}; for (const field of fields) { if (field in data) filtered[field] = data[field]; } return filtered; }; } /** * Check if a field should be included at given verbosity level. * * NOTE: This helper is for determining which FIELDS to include, not for * truncating array contents. If an array is too large, omit the entire * field at lower verbosity levels rather than truncating the array. * * @example * ```typescript * // GOOD: Omit entire field at lower verbosity * const filter = (data, level) => ({ * success: data.success, * ...(shouldInclude(level, 'standard') && { commits: data.commits }) * }); * * // BAD: Don't truncate arrays * // commits: data.commits.slice(0, 10) // ❌ LLM loses context * ``` */ export function shouldInclude( level: VerbosityLevel, minLevel: VerbosityLevel, ): boolean { const levels: VerbosityLevel[] = ['minimal', 'standard', 'full']; return levels.indexOf(level) >= levels.indexOf(minLevel); } /** * Compose multiple filter functions together. */ export function mergeFilters<T>( filters: Array<(data: T, level: VerbosityLevel) => Partial<T> | T>, ): (data: T, level: VerbosityLevel) => Partial<T> | T { return (data: T, level: VerbosityLevel) => { let result: Partial<T> | T = data; for (const filter of filters) { result = filter(result as T, level); } return result; }; } /** * Create a field mapper that transforms/renames fields. */ export function createFieldMapper<T, R = Partial<T>>(mapping: { [K in keyof R]: (data: T, level: VerbosityLevel) => R[K]; }): (data: T, level: VerbosityLevel) => R { return (data: T, level: VerbosityLevel) => { const result = {} as R; for (const [key, transform] of Object.entries(mapping)) { result[key as keyof R] = ( transform as (data: T, level: VerbosityLevel) => R[keyof R] )(data, level); } return result; }; } /** * Create a conditional filter based on runtime conditions. */ export function createConditionalFilter<T>( condition: (data: T) => boolean, trueFilter: (data: T, level: VerbosityLevel) => Partial<T> | T, falseFilter: (data: T, level: VerbosityLevel) => Partial<T> | T, ): (data: T, level: VerbosityLevel) => Partial<T> | T { return (data: T, level: VerbosityLevel) => condition(data) ? trueFilter(data, level) : falseFilter(data, level); } /** * Calculate compression ratio between verbosity levels. * Uses the proper token counting utility for accurate estimates. */ export async function calculateCompressionRatio<T>( data: T, filter?: (data: T, level: VerbosityLevel) => Partial<T> | T, ): Promise<{ minimal: number; standard: number; full: number; minimalSavings: number; standardSavings: number; }> { const levels: VerbosityLevel[] = ['minimal', 'standard', 'full']; const tokens = {} as Record<VerbosityLevel, number>; for (const level of levels) { const filtered = filter ? filter(data, level) : data; const jsonString = JSON.stringify(filtered); tokens[level] = await countTokens(jsonString); } return { minimal: tokens.minimal, standard: tokens.standard, full: tokens.full, minimalSavings: Math.round( ((tokens.full - tokens.minimal) / tokens.full) * 100, ), standardSavings: Math.round( ((tokens.full - tokens.standard) / tokens.full) * 100, ), }; } /** * Default JSON formatter (no verbosity filtering). */ export const defaultJsonFormatter = createJsonFormatter();

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/cyanheads/git-mcp-server'

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