Skip to main content
Glama
response-formatter.js10.1 kB
/** * Response Formatter for MCP Tool Responses * Provides configurable output formats for better integration */ export class ResponseFormatter { constructor(config = {}) { this.config = { // Response format: 'structured', 'json', 'pretty-json' format: config.format || process.env.SQL_SERVER_RESPONSE_FORMAT || 'structured', // Whether to include metadata in responses includeMetadata: config.includeMetadata ?? true, // Whether to include performance metrics includePerformance: config.includePerformance ?? false, // Maximum response size (in characters) before truncation maxResponseSize: config.maxResponseSize || 1000000, // 1MB ...config }; } /** * Formats a query execution result * @param {object} result - Raw query result from mssql * @param {object} context - Execution context * @returns {object} Formatted MCP response */ formatQueryResult(result, context = {}) { const startTime = context.startTime || Date.now(); const endTime = Date.now(); const duration = endTime - startTime; const baseData = { success: true, rowsAffected: result.rowsAffected || [], recordset: result.recordset || [], recordsets: result.recordsets || [] }; // Add metadata if enabled if (this.config.includeMetadata) { baseData.metadata = { query: context.query ? this.truncateQuery(context.query) : undefined, database: context.database, tool: context.toolName, timestamp: new Date().toISOString() }; } // Add performance metrics if enabled if (this.config.includePerformance) { baseData.performance = { duration, queryExecutionTime: duration, resultSize: this.calculateResultSize(baseData.recordset), recordCount: baseData.recordset ? baseData.recordset.length : 0 }; } // Include safety info if (context.securityConfig) { baseData.security = { readOnlyMode: context.securityConfig.readOnlyMode, destructiveOperationsAllowed: context.securityConfig.allowDestructiveOperations, schemaChangesAllowed: context.securityConfig.allowSchemaChanges }; } return this.formatResponse(baseData); } /** * Formats a table data result * @param {object} result - Raw query result * @param {object} context - Execution context * @returns {object} Formatted MCP response */ formatTableData(result, context = {}) { const baseData = { success: true, table: `${context.schema || 'dbo'}.${context.tableName}`, rowCount: result.recordset ? result.recordset.length : 0, data: result.recordset || [], columns: this.extractColumnInfo(result.recordset) }; if (this.config.includeMetadata) { baseData.metadata = { database: context.database, schema: context.schema || 'dbo', table: context.tableName, limit: context.limit, whereClause: context.whereClause, timestamp: new Date().toISOString() }; } return this.formatResponse(baseData); } /** * Formats a CSV export result * @param {string} csvData - CSV content * @param {object} context - Export context * @returns {object} Formatted MCP response */ formatCsvExport(csvData, context = {}) { const baseData = { success: true, table: `${context.schema || 'dbo'}.${context.tableName}`, format: 'csv', csvData: csvData, rowCount: context.rowCount || 0, columnCount: context.columnCount || 0 }; if (this.config.includeMetadata) { baseData.metadata = { database: context.database, schema: context.schema || 'dbo', table: context.tableName, exportSize: csvData.length, timestamp: new Date().toISOString() }; } return this.formatResponse(baseData); } /** * Formats a list result (databases, tables, etc.) * @param {array} items - Array of items * @param {object} context - List context * @returns {object} Formatted MCP response */ formatListResult(items, context = {}) { const baseData = { success: true, items: items || [], count: items ? items.length : 0 }; if (this.config.includeMetadata) { baseData.metadata = { listType: context.listType || 'unknown', database: context.database, schema: context.schema, timestamp: new Date().toISOString() }; } return this.formatResponse(baseData); } /** * Formats an error response * @param {Error} error - Error object * @param {object} context - Error context * @returns {object} Formatted MCP error response */ formatError(error, context = {}) { const errorData = { success: false, error: { name: error.name, message: error.message, code: error.code || 'UNKNOWN_ERROR' } }; if (this.config.includeMetadata) { errorData.metadata = { tool: context.toolName, query: context.query ? this.truncateQuery(context.query) : undefined, database: context.database, timestamp: new Date().toISOString() }; } // Include stack trace in development if (process.env.NODE_ENV !== 'production' && error.stack) { errorData.error.stack = error.stack; } return this.formatResponse(errorData); } /** * Formats the final response based on configured format * @param {object} data - Data to format * @returns {object} MCP-compatible response */ formatResponse(data) { // Check response size and truncate if necessary const responseSize = JSON.stringify(data).length; if (responseSize > this.config.maxResponseSize) { data = this.truncateResponse(data, responseSize); } const content = []; switch (this.config.format) { case 'structured': // Return structured data for programmatic consumption content.push({ type: 'text', text: JSON.stringify(data) }); break; case 'json': // Return compact JSON content.push({ type: 'text', text: JSON.stringify(data) }); break; case 'pretty-json': // Return pretty-printed JSON (original behavior) content.push({ type: 'text', text: JSON.stringify(data, null, 2) }); break; default: // Default to structured format content.push({ type: 'text', text: JSON.stringify(data) }); } return { content }; } /** * Extracts column information from recordset * @param {array} recordset - Query recordset * @returns {array} Column metadata */ extractColumnInfo(recordset) { if (!recordset || recordset.length === 0) { return []; } const firstRow = recordset[0]; return Object.keys(firstRow).map(columnName => ({ name: columnName, type: this.inferColumnType(firstRow[columnName]) })); } /** * Infers column type from sample data * @param {*} value - Sample value * @returns {string} Inferred type */ inferColumnType(value) { if (value === null || value === undefined) return 'null'; if (typeof value === 'number') { return Number.isInteger(value) ? 'integer' : 'decimal'; } if (typeof value === 'boolean') return 'boolean'; if (value instanceof Date) return 'datetime'; if (typeof value === 'string') { // Try to infer more specific string types if (/^\d{4}-\d{2}-\d{2}/.test(value)) return 'date'; if (value.length === 36 && /^[0-9a-f-]+$/i.test(value)) return 'guid'; return 'string'; } return 'unknown'; } /** * Calculates approximate size of result data * @param {*} data - Data to measure * @returns {number} Size in bytes (approximate) */ calculateResultSize(data) { return JSON.stringify(data || {}).length; } /** * Truncates a query for logging/metadata * @param {string} query - SQL query * @param {number} maxLength - Maximum length * @returns {string} Truncated query */ truncateQuery(query, maxLength = 200) { if (!query || query.length <= maxLength) { return query; } return `${query.substring(0, maxLength)}...`; } /** * Truncates response data when it exceeds size limits * @param {object} data - Response data * @param {number} currentSize - Current response size * @returns {object} Truncated response data */ truncateResponse(data, currentSize) { const truncatedData = { ...data }; // Add truncation warning truncatedData.truncated = true; truncatedData.originalSize = currentSize; truncatedData.maxSize = this.config.maxResponseSize; // Truncate recordset data if present if (data.recordset && Array.isArray(data.recordset)) { const estimatedRowSize = JSON.stringify(data.recordset[0] || {}).length; const maxRows = Math.floor(this.config.maxResponseSize / 2 / estimatedRowSize); if (maxRows < data.recordset.length) { truncatedData.recordset = data.recordset.slice(0, maxRows); truncatedData.truncatedRows = data.recordset.length - maxRows; } } // Truncate CSV data if present if (data.csvData && data.csvData.length > this.config.maxResponseSize / 2) { const lines = data.csvData.split('\n'); const headerLine = lines[0]; const maxLines = Math.floor(this.config.maxResponseSize / 2 / (headerLine.length + 10)); truncatedData.csvData = [headerLine, ...lines.slice(1, maxLines)].join('\n'); truncatedData.truncatedLines = lines.length - maxLines - 1; } return truncatedData; } /** * Updates formatter configuration * @param {object} newConfig - New configuration options */ updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; } /** * Gets current configuration * @returns {object} Current configuration */ getConfig() { return { ...this.config }; } }

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/egarcia74/warp-sql-server-mcp'

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