@@ -27,6 +27,15 @@ import {
2727 StandardSqlToolOutput ,
2828} from "@/ibmi-mcp-server/schemas/index.js" ;
2929
30+ // Import formatting utilities
31+ import {
32+ markdown ,
33+ tableFormatter ,
34+ buildColumnAlignmentMap ,
35+ formatColumnHeader ,
36+ type TableStyle ,
37+ } from "@/utils/formatting/index.js" ;
38+
3039/**
3140 * Represents the complete, self-contained definition of an MCP tool.
3241 */
@@ -84,87 +93,137 @@ export interface ToolDefinition<
8493export const standardSqlToolOutputSchema = StandardSqlToolOutputSchema ;
8594
8695/**
87- * Formats SQL tool output into a well-formatted markdown table with metadata.
88- * Provides a user-friendly representation of database query results.
96+ * Configuration options for SQL response formatting.
97+ */
98+ export interface SqlFormatterConfig {
99+ /**
100+ * Table formatting style (default: 'markdown').
101+ */
102+ tableFormat ?: TableStyle ;
103+
104+ /**
105+ * Maximum number of rows to display (default: 100).
106+ */
107+ maxDisplayRows ?: number ;
108+ }
109+
110+ /**
111+ * Formats SQL tool output into a professionally formatted markdown response.
112+ * Uses MarkdownBuilder for structure and TableFormatter for type-aware table rendering.
113+ * Provides column type indicators, NULL tracking, and performance metrics.
114+ *
115+ * @param result - The SQL tool execution result
116+ * @param config - Optional formatting configuration
117+ * @returns Array of content blocks for MCP response
118+ *
119+ * @example
120+ * ```typescript
121+ * const result = await executeSqlTool(params);
122+ * const formatted = sqlResponseFormatter(result, {
123+ * tableFormat: 'grid',
124+ * maxDisplayRows: 50
125+ * });
126+ * ```
89127 */
90128export const sqlResponseFormatter = (
91129 result : StandardSqlToolOutput ,
130+ config ?: SqlFormatterConfig ,
92131) : ContentBlock [ ] => {
132+ const tableFormat = config ?. tableFormat || "markdown" ;
133+ const maxDisplayRows = config ?. maxDisplayRows || 100 ;
134+
135+ // Handle error cases
93136 if ( ! result . success || ! result . data ) {
94- // Handle error cases
95137 const errorMessage = result . error || "Unknown error occurred" ;
96138 const { metadata } = result ;
97139
98- let errorResponse = `❌ **SQL Query Failed**\n\n` ;
140+ const errorBuilder = markdown ( )
141+ . alert ( "caution" , "❌ SQL Query Failed" )
142+ . blankLine ( ) ;
99143
100144 if ( metadata ?. toolName ) {
101- errorResponse += `** Tool:** ${ metadata . toolName } \n\n` ;
145+ errorBuilder . keyValue ( " Tool" , metadata . toolName ) ;
102146 }
103147
104- errorResponse += `** Error:** ${ errorMessage } ` ;
148+ errorBuilder . keyValue ( " Error" , errorMessage ) ;
105149
106150 if ( result . errorCode ) {
107- errorResponse += `\n** Error Code:** ${ result . errorCode } ` ;
151+ errorBuilder . keyValue ( " Error Code" , String ( result . errorCode ) ) ;
108152 }
109153
110154 if ( metadata ?. sqlStatement ) {
111155 const truncatedSql =
112156 metadata . sqlStatement . length > 200
113157 ? metadata . sqlStatement . substring ( 0 , 197 ) + "..."
114158 : metadata . sqlStatement ;
115- errorResponse += `\n\n**SQL Statement:**\n\`\`\`sql\n${ truncatedSql } \n\`\`\`` ;
159+ errorBuilder
160+ . blankLine ( )
161+ . h3 ( "SQL Statement" )
162+ . codeBlock ( truncatedSql , "sql" ) ;
116163 }
117164
118- return [ { type : "text" , text : errorResponse } ] ;
165+ const errorMarkdown = errorBuilder . build ( ) ;
166+
167+ return [ { type : "text" , text : errorMarkdown } ] ;
119168 }
120169
121170 const { data, metadata } = result ;
122171 const rowCount = data . length ;
123- const columnCount = metadata ?. columns ?. length || 0 ;
124172
125- // Build structured response
126- let response = "" ;
173+ // Start building the markdown response
174+ const mdBuilder = markdown ( ) ;
127175
128176 // Tool header
129177 if ( metadata ?. toolName ) {
130- response += `## ${ metadata . toolName } \n\n` ;
178+ mdBuilder . h2 ( metadata . toolName ) ;
131179 }
132180
133- // Success indicator and row count
134- response += `✅ **Query completed successfully**\n\n` ;
135- response += `Found **${ rowCount } row${ rowCount !== 1 ? "s" : "" } ** from the database query\n\n` ;
181+ // Success indicator
182+ mdBuilder
183+ . alert ( "tip" , "✅ Query completed successfully" )
184+ . blankLine ( )
185+ . paragraph (
186+ `Found **${ rowCount } row${ rowCount !== 1 ? "s" : "" } ** from the database query` ,
187+ ) ;
136188
137189 // SQL Statement section
138190 if ( metadata ?. sqlStatement ) {
139191 const truncatedSql =
140192 metadata . sqlStatement . length > 500
141193 ? metadata . sqlStatement . substring ( 0 , 497 ) + "..."
142194 : metadata . sqlStatement ;
143- response += `** SQL Statement:**\n\`\`\`sql\n ${ truncatedSql } \n\`\`\`\n\n` ;
195+ mdBuilder . h3 ( " SQL Statement" ) . codeBlock ( truncatedSql , "sql" ) ;
144196 }
145197
146198 // Parameters section
147199 if ( metadata ?. parameters && Object . keys ( metadata . parameters ) . length > 0 ) {
148- response += `**Parameters:**\n` ;
149- Object . entries ( metadata . parameters ) . forEach ( ( [ key , value ] ) => {
150- const displayValue =
151- value === null || value === undefined
152- ? "NULL"
153- : typeof value === "string" && value . length > 100
154- ? `${ String ( value ) . substring ( 0 , 97 ) } ...`
155- : String ( value ) ;
156- response += `- \`${ key } \`: ${ displayValue } \n` ;
157- } ) ;
158- response += `\n` ;
200+ mdBuilder . h3 ( "Parameters" ) ;
201+ const paramList = Object . entries ( metadata . parameters ) . map (
202+ ( [ key , value ] ) => {
203+ const displayValue =
204+ value === null || value === undefined
205+ ? "NULL"
206+ : typeof value === "string" && value . length > 100
207+ ? `${ String ( value ) . substring ( 0 , 97 ) } ...`
208+ : String ( value ) ;
209+ return `\`${ key } \`: ${ displayValue } ` ;
210+ } ,
211+ ) ;
212+ mdBuilder . list ( paramList ) ;
159213 }
160214
161215 // Handle empty results
162216 if ( rowCount === 0 ) {
163- response += `No rows returned from the query.\n\n` ;
164- response += `**Execution Summary:**\n` ;
165- response += `- Execution time: ${ metadata ?. executionTime ? `${ metadata . executionTime } ms` : "N/A" } \n` ;
166- response += `- Parameters used: ${ metadata ?. parameterCount || 0 } \n` ;
167- return [ { type : "text" , text : response } ] ;
217+ mdBuilder
218+ . paragraph ( "No rows returned from the query." )
219+ . h3 ( "Execution Summary" )
220+ . keyValue (
221+ "Execution time" ,
222+ metadata ?. executionTime ? `${ metadata . executionTime } ms` : "N/A" ,
223+ )
224+ . keyValue ( "Parameters used" , String ( metadata ?. parameterCount || 0 ) ) ;
225+
226+ return [ { type : "text" , text : mdBuilder . build ( ) } ] ;
168227 }
169228
170229 // Extract column information
@@ -179,68 +238,90 @@ export const sqlResponseFormatter = (
179238 label : key ,
180239 } ) ) ;
181240
182- // Build markdown table
183- let tableMarkdown = "" ;
184-
185- // Header row with cleaner formatting
186- const headers = allColumns . map ( ( col ) => {
187- const header = col . label || col . name ;
188- return col . type ? `${ header } (${ col . type } )` : header ;
189- } ) ;
190- tableMarkdown += `| ${ headers . join ( " | " ) } |\n` ;
241+ // Prepare data for table formatter
242+ const displayRows = data . slice ( 0 , maxDisplayRows ) ;
243+ const columnCount = allColumns . length ;
191244
192- // Separator row
193- tableMarkdown += `|${ headers . map ( ( ) => "----------" ) . join ( "|" ) } |\n` ;
245+ // Format headers with type indicators
246+ const headers = allColumns . map ( ( col ) =>
247+ formatColumnHeader ( col . label || col . name , col . type ) ,
248+ ) ;
194249
195- // Data rows (limit to first 500 rows for better performance)
196- const maxDisplayRows = 500 ;
197- const displayRows = data . slice ( 0 , maxDisplayRows ) ;
250+ // Build column alignment map based on types
251+ const alignment = buildColumnAlignmentMap ( allColumns ) ;
198252
199- displayRows . forEach ( ( row ) => {
200- const values = allColumns . map ( ( col ) => {
253+ // Convert data rows to string arrays
254+ const rows = displayRows . map ( ( row ) =>
255+ allColumns . map ( ( col ) => {
201256 const value = row [ col . name ] ;
202- if ( value === null || value === undefined ) return "NULL" ;
257+ if ( value === null || value === undefined ) return null ;
203258 return String ( value ) ;
204- } ) ;
205- tableMarkdown += `| ${ values . join ( " | " ) } |\n` ;
259+ } ) ,
260+ ) ;
261+
262+ // Format table with metadata tracking
263+ const tableResult = tableFormatter . formatRawWithMetadata ( headers , rows , {
264+ style : tableFormat ,
265+ alignment,
266+ nullReplacement : "-" ,
267+ maxWidth : 50 ,
268+ truncate : true ,
206269 } ) ;
207270
208- // Show row limitation notice if applicable
271+ // Add truncation notice if applicable
209272 if ( displayRows . length < rowCount ) {
210- response += `*Showing first ${ displayRows . length } of ${ rowCount } rows*\n\n` ;
273+ mdBuilder . alert (
274+ "note" ,
275+ `Showing ${ displayRows . length } of ${ rowCount } rows. ${ rowCount - displayRows . length } rows omitted.` ,
276+ ) ;
211277 }
212278
213- response += `**Results:**\n\n${ tableMarkdown } \n` ;
279+ // Results section
280+ mdBuilder . h3 ( "Results" ) . raw ( tableResult . table ) ;
214281
215- // Count null values across all displayed data
216- let nullCount = 0 ;
217- displayRows . forEach ( ( row ) => {
218- allColumns . forEach ( ( col ) => {
219- if ( row [ col . name ] === null || row [ col . name ] === undefined ) {
220- nullCount ++ ;
221- }
222- } ) ;
223- } ) ;
282+ // NULL value summary (only if there are NULLs)
283+ const nullCounts = tableResult . metadata . nullCounts ;
284+ const totalNulls = Object . values ( nullCounts ) . reduce (
285+ ( sum , count ) => sum + count ,
286+ 0 ,
287+ ) ;
224288
225- // Execution summary
226- response += `**Summary:**\n` ;
227- response += `- Total rows: ${ rowCount } \n` ;
228- response += `- Columns: ${ columnCount } \n` ;
229- response += `- Null values: ${ nullCount } \n` ;
289+ // Summary section
290+ const summaryItems : string [ ] = [
291+ `** Total rows** : ${ rowCount } ` ,
292+ `** Columns** : ${ columnCount } ` ,
293+ ] ;
230294
231295 if ( metadata ?. executionTime ) {
232- response += `- Execution time: ${ metadata . executionTime } ms\n` ;
296+ summaryItems . push ( `**Execution time**: ${ metadata . executionTime } ms` ) ;
297+ }
298+
299+ if ( totalNulls > 0 ) {
300+ const nullDetails = Object . entries ( nullCounts )
301+ . filter ( ( [ , count ] ) => count > 0 )
302+ . map ( ( [ col , count ] ) => {
303+ // Convert column index to name for display
304+ const colIndex = parseInt ( col ) ;
305+ const colName = isNaN ( colIndex )
306+ ? col
307+ : allColumns [ colIndex ] ?. name || col ;
308+ return `${ colName } (${ count } )` ;
309+ } )
310+ . join ( ", " ) ;
311+ summaryItems . push ( `**NULL values**: ${ totalNulls } total - ${ nullDetails } ` ) ;
233312 }
234313
235314 if ( metadata ?. affectedRows !== undefined ) {
236- response += `- Affected rows: ${ metadata . affectedRows } \n` ;
315+ summaryItems . push ( `** Affected rows** : ${ metadata . affectedRows } ` ) ;
237316 }
238317
239318 if ( metadata ?. parameterCount ) {
240- response += `- Parameters processed: ${ metadata . parameterCount } \n` ;
319+ summaryItems . push ( `** Parameters processed** : ${ metadata . parameterCount } ` ) ;
241320 }
242321
243- return [ { type : "text" , text : response } ] ;
322+ mdBuilder . h3 ( "Summary" ) . list ( summaryItems ) ;
323+
324+ return [ { type : "text" , text : mdBuilder . build ( ) } ] ;
244325} ;
245326
246327/**
0 commit comments