From 0716f9c330f9ae2fc80f64d0bc9b49ccd1feef88 Mon Sep 17 00:00:00 2001 From: Ethan Setnik Date: Wed, 8 Apr 2026 14:36:03 -0400 Subject: [PATCH 1/2] feat(sqlserver): surface informational messages (STATISTICS, PRINT) in query results SQL Server emits informational messages via the TDS protocol for SET STATISTICS TIME/IO output, PRINT statements, and warnings. The mssql driver's Request object exposes these through 'info' events, but they were previously discarded. This change captures those messages during query execution and includes them in the response when present, enabling MCP clients to access query performance statistics and diagnostic output. Changes: - Add optional `messages` field to SQLResult interface - Listen for 'info' events on SQL Server Request before execution - Include non-empty messages array in execute_sql tool response --- src/connectors/interface.ts | 2 ++ src/connectors/sqlserver/index.ts | 8 +++++++- src/tools/execute-sql.ts | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/connectors/interface.ts b/src/connectors/interface.ts index 2daaefd4..bd848b85 100644 --- a/src/connectors/interface.ts +++ b/src/connectors/interface.ts @@ -10,6 +10,8 @@ export type ConnectorType = "postgres" | "mysql" | "mariadb" | "sqlite" | "sqlse export interface SQLResult { rows: any[]; rowCount: number; + /** Informational messages from the database (e.g. SQL Server STATISTICS TIME/IO, PRINT output) */ + messages?: string[]; } export interface TableColumn { diff --git a/src/connectors/sqlserver/index.ts b/src/connectors/sqlserver/index.ts index b8cdc1e2..a2e8530d 100644 --- a/src/connectors/sqlserver/index.ts +++ b/src/connectors/sqlserver/index.ts @@ -581,8 +581,13 @@ export class SQLServerConnector implements Connector { processedSQL = SQLRowLimiter.applyMaxRowsForSQLServer(sqlQuery, options.maxRows); } - // Create request and add parameters if provided + // Create request and collect informational messages (e.g. SET STATISTICS TIME/IO, PRINT) const request = this.connection.request(); + const messages: string[] = []; + request.on('info', (info: { message: string }) => { + messages.push(info.message); + }); + if (parameters && parameters.length > 0) { // SQL Server uses @p1, @p2, etc. for parameters parameters.forEach((param, index) => { @@ -625,6 +630,7 @@ export class SQLServerConnector implements Connector { return { rows: result.recordset || [], rowCount: result.rowsAffected[0] || 0, + ...(messages.length > 0 ? { messages } : {}), }; } catch (error) { throw new Error(`Failed to execute query: ${(error as Error).message}`); diff --git a/src/tools/execute-sql.ts b/src/tools/execute-sql.ts index b6def2e6..c07b73c1 100644 --- a/src/tools/execute-sql.ts +++ b/src/tools/execute-sql.ts @@ -74,6 +74,7 @@ export function createExecuteSqlToolHandler(sourceId?: string) { rows: result.rows, count: result.rowCount, source_id: effectiveSourceId, + ...(result.messages && result.messages.length > 0 ? { messages: result.messages } : {}), }; return createToolSuccessResponse(responseData); From 4cbd8cae918c72d09caaf23c0f3589238d09e72f Mon Sep 17 00:00:00 2001 From: Ethan Setnik Date: Wed, 8 Apr 2026 15:05:14 -0400 Subject: [PATCH 2/2] test(sqlserver): add integration tests for informational messages capture Adds three tests covering the new messages behavior: - PRINT output is captured and returned in messages array - SET STATISTICS TIME output contains CPU/elapsed timing info - messages field is absent when no informational messages are emitted --- .../__tests__/sqlserver.integration.test.ts | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/connectors/__tests__/sqlserver.integration.test.ts b/src/connectors/__tests__/sqlserver.integration.test.ts index f2da24be..acbc260c 100644 --- a/src/connectors/__tests__/sqlserver.integration.test.ts +++ b/src/connectors/__tests__/sqlserver.integration.test.ts @@ -574,6 +574,46 @@ describe('SQL Server Connector Integration Tests', () => { expect(result.rows[0]).toHaveProperty('age_rank'); }); + it('should capture PRINT output in messages', async () => { + const result = await sqlServerTest.connector.executeSQL( + "PRINT 'hello from sql server'; SELECT 1 as value;", + {} + ); + + expect(result.rows).toHaveLength(1); + expect(result.rows[0].value).toBe(1); + expect(result.messages).toBeDefined(); + expect(result.messages!.length).toBeGreaterThan(0); + expect(result.messages).toContain('hello from sql server'); + }); + + it('should capture SET STATISTICS TIME output in messages', async () => { + const result = await sqlServerTest.connector.executeSQL( + 'SET STATISTICS TIME ON; SELECT COUNT(*) as cnt FROM users; SET STATISTICS TIME OFF;', + {} + ); + + expect(result.rows).toHaveLength(1); + expect(result.messages).toBeDefined(); + expect(result.messages!.length).toBeGreaterThan(0); + // STATISTICS TIME emits messages containing "CPU time" and "elapsed time" + const hasTimingMessage = result.messages!.some( + msg => msg.includes('CPU time') || msg.includes('elapsed time') + ); + expect(hasTimingMessage).toBe(true); + }); + + it('should not include messages field when no informational messages are emitted', async () => { + const result = await sqlServerTest.connector.executeSQL( + 'SELECT 1 as value', + {} + ); + + expect(result.rows).toHaveLength(1); + // messages should be undefined (not present) when no info messages were emitted + expect(result.messages).toBeUndefined(); + }); + it('should ignore maxRows when not specified', async () => { // Test without maxRows - should return all rows const result = await sqlServerTest.connector.executeSQL(