diff --git a/src/connectors/__tests__/sqlserver.integration.test.ts b/src/connectors/__tests__/sqlserver.integration.test.ts index f2da24b..acbc260 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( diff --git a/src/connectors/interface.ts b/src/connectors/interface.ts index 2daaefd..bd848b8 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 b8cdc1e..a2e8530 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 b6def2e..c07b73c 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);