mcp-consultant-tools 6.0.0 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +25 -0
- package/README.md +72 -5
- package/build/AzureSqlService.js +497 -0
- package/build/LogAnalyticsService.js +409 -0
- package/build/index.js +1153 -0
- package/build/utils/loganalytics-formatters.js +371 -0
- package/build/utils/sql-formatters.js +180 -0
- package/package.json +7 -2
package/LICENSE
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
+
Copyright (c) 2025 Klemens Stelk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
================================================================================
|
|
24
|
+
This project includes code derived from powerplatform-mcp
|
|
25
|
+
(https://github.com/michsob/powerplatform-mcp)
|
|
26
|
+
|
|
27
|
+
Original Copyright Notice:
|
|
3
28
|
Copyright (c) 2025 Michal Sobieraj
|
|
4
29
|
|
|
5
30
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# MCP Consultant Tools
|
|
2
2
|
|
|
3
|
-
A Model Context Protocol (MCP) server providing intelligent access to PowerPlatform/Dataverse, Azure DevOps, Figma,
|
|
3
|
+
A Model Context Protocol (MCP) server providing intelligent access to PowerPlatform/Dataverse, Azure DevOps, Figma, Azure Application Insights, Azure Log Analytics, and Azure SQL Database through an AI-friendly interface.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
@@ -16,10 +16,12 @@ This MCP server enables AI assistants to:
|
|
|
16
16
|
- **Azure DevOps** (12 tools): Search wikis, manage work items, execute WIQL queries
|
|
17
17
|
- **Figma** (2 tools): Extract design data in simplified, AI-friendly format
|
|
18
18
|
- **Application Insights** (10 tools): Query telemetry, analyze exceptions, monitor performance, troubleshoot issues
|
|
19
|
+
- **Log Analytics** (10 tools): Query Azure Functions logs, analyze errors, monitor function performance, search workspace logs
|
|
20
|
+
- **Azure SQL Database** (9 tools): Explore database schema, query tables safely with read-only access, investigate database structure
|
|
19
21
|
|
|
20
22
|
All integrations are **optional** - configure only the services you need.
|
|
21
23
|
|
|
22
|
-
**Total:
|
|
24
|
+
**Total: 116 MCP tools & 23 prompts** providing comprehensive access to your development and operations lifecycle.
|
|
23
25
|
|
|
24
26
|
## Known limitations
|
|
25
27
|
- Cannot create Model-Driven-Apps
|
|
@@ -78,7 +80,19 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
|
|
|
78
80
|
"APPINSIGHTS_TENANT_ID": "your-tenant-id",
|
|
79
81
|
"APPINSIGHTS_CLIENT_ID": "your-client-id",
|
|
80
82
|
"APPINSIGHTS_CLIENT_SECRET": "your-client-secret",
|
|
81
|
-
"APPINSIGHTS_RESOURCES": "[{\"id\":\"prod-api\",\"name\":\"Production API\",\"appId\":\"your-app-id\",\"active\":true}]"
|
|
83
|
+
"APPINSIGHTS_RESOURCES": "[{\"id\":\"prod-api\",\"name\":\"Production API\",\"appId\":\"your-app-id\",\"active\":true}]",
|
|
84
|
+
|
|
85
|
+
"LOGANALYTICS_AUTH_METHOD": "entra-id",
|
|
86
|
+
"LOGANALYTICS_TENANT_ID": "your-tenant-id",
|
|
87
|
+
"LOGANALYTICS_CLIENT_ID": "your-client-id",
|
|
88
|
+
"LOGANALYTICS_CLIENT_SECRET": "your-client-secret",
|
|
89
|
+
"LOGANALYTICS_RESOURCES": "[{\"id\":\"prod-functions\",\"name\":\"Production Functions\",\"workspaceId\":\"your-workspace-id\",\"active\":true}]",
|
|
90
|
+
|
|
91
|
+
"AZURE_SQL_SERVER": "yourserver.database.windows.net",
|
|
92
|
+
"AZURE_SQL_DATABASE": "yourdatabase",
|
|
93
|
+
"AZURE_SQL_USERNAME": "your-username",
|
|
94
|
+
"AZURE_SQL_PASSWORD": "your-password",
|
|
95
|
+
"AZURE_SQL_USE_AZURE_AD": "false"
|
|
82
96
|
}
|
|
83
97
|
}
|
|
84
98
|
}
|
|
@@ -121,7 +135,19 @@ Create `.vscode/mcp.json` in your project:
|
|
|
121
135
|
"APPINSIGHTS_TENANT_ID": "your-tenant-id",
|
|
122
136
|
"APPINSIGHTS_CLIENT_ID": "your-client-id",
|
|
123
137
|
"APPINSIGHTS_CLIENT_SECRET": "your-client-secret",
|
|
124
|
-
"APPINSIGHTS_RESOURCES": "[{\"id\":\"prod-api\",\"name\":\"Production API\",\"appId\":\"your-app-id\",\"active\":true}]"
|
|
138
|
+
"APPINSIGHTS_RESOURCES": "[{\"id\":\"prod-api\",\"name\":\"Production API\",\"appId\":\"your-app-id\",\"active\":true}]",
|
|
139
|
+
|
|
140
|
+
"LOGANALYTICS_AUTH_METHOD": "entra-id",
|
|
141
|
+
"LOGANALYTICS_TENANT_ID": "your-tenant-id",
|
|
142
|
+
"LOGANALYTICS_CLIENT_ID": "your-client-id",
|
|
143
|
+
"LOGANALYTICS_CLIENT_SECRET": "your-client-secret",
|
|
144
|
+
"LOGANALYTICS_RESOURCES": "[{\"id\":\"prod-functions\",\"name\":\"Production Functions\",\"workspaceId\":\"your-workspace-id\",\"active\":true}]",
|
|
145
|
+
|
|
146
|
+
"AZURE_SQL_SERVER": "yourserver.database.windows.net",
|
|
147
|
+
"AZURE_SQL_DATABASE": "yourdatabase",
|
|
148
|
+
"AZURE_SQL_USERNAME": "your-username",
|
|
149
|
+
"AZURE_SQL_PASSWORD": "your-password",
|
|
150
|
+
"AZURE_SQL_USE_AZURE_AD": "false"
|
|
125
151
|
}
|
|
126
152
|
}
|
|
127
153
|
}
|
|
@@ -293,10 +319,15 @@ The server includes **18 prompts** that provide formatted, context-rich output:
|
|
|
293
319
|
- `appinsights-availability-report` - Availability and uptime report
|
|
294
320
|
- `appinsights-troubleshooting-guide` - Comprehensive troubleshooting guide combining all telemetry
|
|
295
321
|
|
|
322
|
+
**Azure SQL Database:**
|
|
323
|
+
- `sql-database-overview` - Comprehensive database schema overview with all objects
|
|
324
|
+
- `sql-table-details` - Detailed table report with columns, indexes, and relationships
|
|
325
|
+
- `sql-query-results` - Formatted query results with column headers
|
|
326
|
+
|
|
296
327
|
## Documentation
|
|
297
328
|
|
|
298
329
|
- **[SETUP.md](SETUP.md)** - Complete setup guide with credentials, troubleshooting, and security
|
|
299
|
-
- **[TOOLS.md](TOOLS.md)** - Full reference for all
|
|
330
|
+
- **[TOOLS.md](TOOLS.md)** - Full reference for all 105+ tools and 21 prompts
|
|
300
331
|
- **[USAGE.md](USAGE.md)** - Examples and use cases for all integrations
|
|
301
332
|
- **[CLAUDE.md](CLAUDE.md)** - Architecture details and development guide
|
|
302
333
|
|
|
@@ -376,6 +407,28 @@ All integrations are optional and can be configured independently:
|
|
|
376
407
|
- Requires Personal Access Token or OAuth
|
|
377
408
|
- Read-only access to design files
|
|
378
409
|
|
|
410
|
+
**Azure SQL Database:**
|
|
411
|
+
- Supports SQL Authentication or Azure AD authentication
|
|
412
|
+
- **Read-only access by design** - only SELECT queries permitted
|
|
413
|
+
- Safety mechanisms:
|
|
414
|
+
- Query validation blocks INSERT, UPDATE, DELETE, DROP, EXEC, and other write operations
|
|
415
|
+
- 10MB response size limit to prevent memory exhaustion
|
|
416
|
+
- 1000 row result limit (configurable)
|
|
417
|
+
- 30-second query timeout protection
|
|
418
|
+
- Connection pooling with health checks (max 10 connections)
|
|
419
|
+
- Credential sanitization in error messages
|
|
420
|
+
- Audit logging for all user queries
|
|
421
|
+
- Recommended database permissions:
|
|
422
|
+
```sql
|
|
423
|
+
ALTER ROLE db_datareader ADD MEMBER [mcp_readonly];
|
|
424
|
+
GRANT VIEW DEFINITION TO [mcp_readonly];
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
**Application Insights:**
|
|
428
|
+
- Supports Entra ID (OAuth) or API Key authentication
|
|
429
|
+
- Read-only access to telemetry data
|
|
430
|
+
- Entra ID recommended for production (higher rate limits)
|
|
431
|
+
|
|
379
432
|
See [SETUP.md](SETUP.md#security-best-practices) for security best practices.
|
|
380
433
|
|
|
381
434
|
## Examples
|
|
@@ -412,6 +465,20 @@ AI: [uses wiki-search-results prompt]
|
|
|
412
465
|
Returns formatted search results with snippets
|
|
413
466
|
```
|
|
414
467
|
|
|
468
|
+
### Investigate SQL Database schema
|
|
469
|
+
|
|
470
|
+
```
|
|
471
|
+
User: "Show me the database schema overview"
|
|
472
|
+
AI: [uses sql-database-overview prompt]
|
|
473
|
+
Returns formatted overview with tables, views, procedures, and statistics
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
```
|
|
477
|
+
User: "Query the Users table for active accounts"
|
|
478
|
+
AI: [uses sql-execute-query tool with "SELECT TOP 10 * FROM dbo.Users WHERE IsActive = 1"]
|
|
479
|
+
Returns formatted query results with column headers
|
|
480
|
+
```
|
|
481
|
+
|
|
415
482
|
See [USAGE.md](USAGE.md) for more examples.
|
|
416
483
|
|
|
417
484
|
## Development
|
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
import sql from 'mssql';
|
|
2
|
+
import { auditLogger } from './utils/audit-logger.js';
|
|
3
|
+
// Configuration constants
|
|
4
|
+
const MAX_RESPONSE_SIZE_BYTES = 10 * 1024 * 1024; // 10MB
|
|
5
|
+
/**
|
|
6
|
+
* Azure SQL Database Service
|
|
7
|
+
*
|
|
8
|
+
* Provides read-only access to Azure SQL Database for investigation and analysis.
|
|
9
|
+
* Implements security controls including query validation, result limits, and audit logging.
|
|
10
|
+
*/
|
|
11
|
+
export class AzureSqlService {
|
|
12
|
+
config;
|
|
13
|
+
pool = null;
|
|
14
|
+
constructor(config) {
|
|
15
|
+
this.config = {
|
|
16
|
+
...config,
|
|
17
|
+
port: config.port || 1433,
|
|
18
|
+
queryTimeout: config.queryTimeout || 30000,
|
|
19
|
+
maxResultRows: config.maxResultRows || 1000,
|
|
20
|
+
connectionTimeout: config.connectionTimeout || 15000,
|
|
21
|
+
poolMin: config.poolMin || 0,
|
|
22
|
+
poolMax: config.poolMax || 10,
|
|
23
|
+
useAzureAd: config.useAzureAd ?? false,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Initialize connection pool on demand with health checks
|
|
28
|
+
*/
|
|
29
|
+
async getPool() {
|
|
30
|
+
// Check if pool exists, is connected, and is healthy
|
|
31
|
+
if (this.pool && this.pool.connected && this.pool.healthy) {
|
|
32
|
+
return this.pool;
|
|
33
|
+
}
|
|
34
|
+
// Close unhealthy pool if it exists
|
|
35
|
+
if (this.pool && !this.pool.healthy) {
|
|
36
|
+
try {
|
|
37
|
+
await this.pool.close();
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
console.error('Error closing unhealthy pool:', error);
|
|
41
|
+
}
|
|
42
|
+
this.pool = null;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const poolConfig = {
|
|
46
|
+
server: this.config.server,
|
|
47
|
+
database: this.config.database,
|
|
48
|
+
port: this.config.port,
|
|
49
|
+
connectionTimeout: this.config.connectionTimeout,
|
|
50
|
+
requestTimeout: this.config.queryTimeout,
|
|
51
|
+
pool: {
|
|
52
|
+
min: this.config.poolMin,
|
|
53
|
+
max: this.config.poolMax,
|
|
54
|
+
idleTimeoutMillis: 30000,
|
|
55
|
+
},
|
|
56
|
+
options: {
|
|
57
|
+
encrypt: true, // Required for Azure SQL
|
|
58
|
+
trustServerCertificate: false,
|
|
59
|
+
enableArithAbort: true,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
if (this.config.useAzureAd) {
|
|
63
|
+
// Azure AD Authentication
|
|
64
|
+
poolConfig.authentication = {
|
|
65
|
+
type: 'azure-active-directory-service-principal-secret',
|
|
66
|
+
options: {
|
|
67
|
+
clientId: this.config.clientId,
|
|
68
|
+
clientSecret: this.config.clientSecret,
|
|
69
|
+
tenantId: this.config.tenantId,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
// SQL Authentication
|
|
75
|
+
poolConfig.user = this.config.username;
|
|
76
|
+
poolConfig.password = this.config.password;
|
|
77
|
+
}
|
|
78
|
+
this.pool = await sql.connect(poolConfig);
|
|
79
|
+
console.error('Azure SQL connection pool established');
|
|
80
|
+
return this.pool;
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
console.error('Failed to connect to Azure SQL Database:', {
|
|
84
|
+
server: this.config.server,
|
|
85
|
+
database: this.config.database,
|
|
86
|
+
error: this.sanitizeErrorMessage(error.message),
|
|
87
|
+
});
|
|
88
|
+
throw new Error(`Database connection failed: ${this.sanitizeErrorMessage(error.message)}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Sanitize error messages to prevent credential leakage
|
|
93
|
+
*/
|
|
94
|
+
sanitizeErrorMessage(message) {
|
|
95
|
+
return message
|
|
96
|
+
.replace(/password=[^;]+/gi, 'password=***')
|
|
97
|
+
.replace(/pwd=[^;]+/gi, 'pwd=***')
|
|
98
|
+
.replace(/clientSecret=[^;]+/gi, 'clientSecret=***')
|
|
99
|
+
.replace(/Authentication=ActiveDirectoryServicePrincipal;([^;]*);/gi, 'Authentication=***;');
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Execute a query with safety limits and size protection
|
|
103
|
+
*/
|
|
104
|
+
async executeQuery(query, parameters) {
|
|
105
|
+
try {
|
|
106
|
+
const pool = await this.getPool();
|
|
107
|
+
const request = pool.request();
|
|
108
|
+
// Add parameters if provided
|
|
109
|
+
if (parameters) {
|
|
110
|
+
for (const [key, value] of Object.entries(parameters)) {
|
|
111
|
+
request.input(key, value);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const result = await request.query(query);
|
|
115
|
+
const rows = result.recordset || [];
|
|
116
|
+
const columns = result.recordset?.columns
|
|
117
|
+
? Object.keys(result.recordset.columns)
|
|
118
|
+
: [];
|
|
119
|
+
// Check response size BEFORE processing
|
|
120
|
+
const jsonSize = JSON.stringify(rows).length;
|
|
121
|
+
if (jsonSize > MAX_RESPONSE_SIZE_BYTES) {
|
|
122
|
+
throw new Error(`Query results too large (${(jsonSize / 1024 / 1024).toFixed(2)} MB). ` +
|
|
123
|
+
`Maximum allowed: ${MAX_RESPONSE_SIZE_BYTES / 1024 / 1024} MB. ` +
|
|
124
|
+
`Add WHERE clause or SELECT specific columns to reduce result size.`);
|
|
125
|
+
}
|
|
126
|
+
// Enforce row limit
|
|
127
|
+
const truncated = rows.length > this.config.maxResultRows;
|
|
128
|
+
const limitedRows = rows.slice(0, this.config.maxResultRows);
|
|
129
|
+
return {
|
|
130
|
+
columns,
|
|
131
|
+
rows: limitedRows,
|
|
132
|
+
rowCount: limitedRows.length,
|
|
133
|
+
truncated,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
console.error('SQL query execution failed:', {
|
|
138
|
+
error: this.sanitizeErrorMessage(error.message),
|
|
139
|
+
query: query.substring(0, 200), // Log first 200 chars
|
|
140
|
+
});
|
|
141
|
+
// Provide user-friendly error messages
|
|
142
|
+
if (error.message.includes('timeout') || error.message.includes('ETIMEDOUT')) {
|
|
143
|
+
throw new Error(`Query timeout exceeded (${this.config.queryTimeout}ms). ` +
|
|
144
|
+
`Try simplifying your query or adding WHERE clause filters.`);
|
|
145
|
+
}
|
|
146
|
+
if (error.message.includes('permission denied') || error.message.includes('denied')) {
|
|
147
|
+
throw new Error('Permission denied. Ensure the database user has SELECT permissions ' +
|
|
148
|
+
'on the requested objects.');
|
|
149
|
+
}
|
|
150
|
+
throw new Error(`Query execution failed: ${this.sanitizeErrorMessage(error.message)}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Close connection pool (cleanup)
|
|
155
|
+
*/
|
|
156
|
+
async close() {
|
|
157
|
+
if (this.pool) {
|
|
158
|
+
try {
|
|
159
|
+
await this.pool.close();
|
|
160
|
+
this.pool = null;
|
|
161
|
+
console.error('Azure SQL connection pool closed');
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
console.error('Error closing connection pool:', this.sanitizeErrorMessage(error.message));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Test database connectivity
|
|
170
|
+
*/
|
|
171
|
+
async testConnection() {
|
|
172
|
+
try {
|
|
173
|
+
const pool = await this.getPool();
|
|
174
|
+
const result = await pool.request().query(`
|
|
175
|
+
SELECT
|
|
176
|
+
@@VERSION as sqlVersion,
|
|
177
|
+
DB_NAME() as currentDatabase,
|
|
178
|
+
SUSER_SNAME() as loginName,
|
|
179
|
+
USER_NAME() as userName
|
|
180
|
+
`);
|
|
181
|
+
return {
|
|
182
|
+
connected: true,
|
|
183
|
+
server: this.config.server,
|
|
184
|
+
database: this.config.database,
|
|
185
|
+
sqlVersion: result.recordset[0].sqlVersion,
|
|
186
|
+
currentDatabase: result.recordset[0].currentDatabase,
|
|
187
|
+
loginName: result.recordset[0].loginName,
|
|
188
|
+
userName: result.recordset[0].userName,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
return {
|
|
193
|
+
connected: false,
|
|
194
|
+
server: this.config.server,
|
|
195
|
+
database: this.config.database,
|
|
196
|
+
error: this.sanitizeErrorMessage(error.message),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* List all user tables in the database
|
|
202
|
+
*/
|
|
203
|
+
async listTables() {
|
|
204
|
+
const query = `
|
|
205
|
+
SELECT
|
|
206
|
+
t.TABLE_SCHEMA as schemaName,
|
|
207
|
+
t.TABLE_NAME as tableName,
|
|
208
|
+
p.rows as rowCount,
|
|
209
|
+
CAST(SUM(a.total_pages) * 8 / 1024.0 AS DECIMAL(10,2)) as sizeMB
|
|
210
|
+
FROM INFORMATION_SCHEMA.TABLES t
|
|
211
|
+
LEFT JOIN sys.tables st ON t.TABLE_NAME = st.name
|
|
212
|
+
LEFT JOIN sys.partitions p ON st.object_id = p.object_id AND p.index_id IN (0,1)
|
|
213
|
+
LEFT JOIN sys.allocation_units a ON p.partition_id = a.container_id
|
|
214
|
+
WHERE t.TABLE_TYPE = 'BASE TABLE'
|
|
215
|
+
AND t.TABLE_SCHEMA != 'sys'
|
|
216
|
+
GROUP BY t.TABLE_SCHEMA, t.TABLE_NAME, p.rows
|
|
217
|
+
ORDER BY t.TABLE_SCHEMA, t.TABLE_NAME
|
|
218
|
+
`;
|
|
219
|
+
const result = await this.executeQuery(query);
|
|
220
|
+
return result.rows;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* List all views in the database
|
|
224
|
+
*/
|
|
225
|
+
async listViews() {
|
|
226
|
+
const query = `
|
|
227
|
+
SELECT
|
|
228
|
+
TABLE_SCHEMA as schemaName,
|
|
229
|
+
TABLE_NAME as viewName,
|
|
230
|
+
VIEW_DEFINITION as definition
|
|
231
|
+
FROM INFORMATION_SCHEMA.VIEWS
|
|
232
|
+
WHERE TABLE_SCHEMA != 'sys'
|
|
233
|
+
ORDER BY TABLE_SCHEMA, TABLE_NAME
|
|
234
|
+
`;
|
|
235
|
+
const result = await this.executeQuery(query);
|
|
236
|
+
return result.rows;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* List all stored procedures
|
|
240
|
+
*/
|
|
241
|
+
async listStoredProcedures() {
|
|
242
|
+
const query = `
|
|
243
|
+
SELECT
|
|
244
|
+
ROUTINE_SCHEMA as schemaName,
|
|
245
|
+
ROUTINE_NAME as procedureName,
|
|
246
|
+
CREATED as createdDate,
|
|
247
|
+
LAST_ALTERED as modifiedDate
|
|
248
|
+
FROM INFORMATION_SCHEMA.ROUTINES
|
|
249
|
+
WHERE ROUTINE_TYPE = 'PROCEDURE'
|
|
250
|
+
AND ROUTINE_SCHEMA != 'sys'
|
|
251
|
+
ORDER BY ROUTINE_SCHEMA, ROUTINE_NAME
|
|
252
|
+
`;
|
|
253
|
+
const result = await this.executeQuery(query);
|
|
254
|
+
return result.rows;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* List all database triggers
|
|
258
|
+
*/
|
|
259
|
+
async listTriggers() {
|
|
260
|
+
const query = `
|
|
261
|
+
SELECT
|
|
262
|
+
s.name as schemaName,
|
|
263
|
+
t.name as triggerName,
|
|
264
|
+
OBJECT_NAME(t.parent_id) as objectName,
|
|
265
|
+
CASE
|
|
266
|
+
WHEN OBJECTPROPERTY(t.object_id, 'ExecIsInsertTrigger') = 1 THEN 'INSERT'
|
|
267
|
+
WHEN OBJECTPROPERTY(t.object_id, 'ExecIsUpdateTrigger') = 1 THEN 'UPDATE'
|
|
268
|
+
WHEN OBJECTPROPERTY(t.object_id, 'ExecIsDeleteTrigger') = 1 THEN 'DELETE'
|
|
269
|
+
ELSE 'UNKNOWN'
|
|
270
|
+
END as triggerEvent,
|
|
271
|
+
t.is_disabled as isDisabled,
|
|
272
|
+
t.create_date as createdDate,
|
|
273
|
+
t.modify_date as modifiedDate
|
|
274
|
+
FROM sys.triggers t
|
|
275
|
+
INNER JOIN sys.objects o ON t.parent_id = o.object_id
|
|
276
|
+
INNER JOIN sys.schemas s ON o.schema_id = s.schema_id
|
|
277
|
+
WHERE t.parent_class = 1 -- Object triggers (not database triggers)
|
|
278
|
+
ORDER BY s.name, t.name
|
|
279
|
+
`;
|
|
280
|
+
const result = await this.executeQuery(query);
|
|
281
|
+
return result.rows;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* List all user-defined functions
|
|
285
|
+
*/
|
|
286
|
+
async listFunctions() {
|
|
287
|
+
const query = `
|
|
288
|
+
SELECT
|
|
289
|
+
ROUTINE_SCHEMA as schemaName,
|
|
290
|
+
ROUTINE_NAME as functionName,
|
|
291
|
+
DATA_TYPE as returnType,
|
|
292
|
+
CREATED as createdDate,
|
|
293
|
+
LAST_ALTERED as modifiedDate
|
|
294
|
+
FROM INFORMATION_SCHEMA.ROUTINES
|
|
295
|
+
WHERE ROUTINE_TYPE = 'FUNCTION'
|
|
296
|
+
AND ROUTINE_SCHEMA != 'sys'
|
|
297
|
+
ORDER BY ROUTINE_SCHEMA, ROUTINE_NAME
|
|
298
|
+
`;
|
|
299
|
+
const result = await this.executeQuery(query);
|
|
300
|
+
return result.rows;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Get detailed schema information for a table
|
|
304
|
+
*/
|
|
305
|
+
async getTableSchema(schemaName, tableName) {
|
|
306
|
+
// First, verify table exists
|
|
307
|
+
const existsQuery = `
|
|
308
|
+
SELECT 1 FROM INFORMATION_SCHEMA.TABLES
|
|
309
|
+
WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = @table
|
|
310
|
+
`;
|
|
311
|
+
const existsResult = await this.executeQuery(existsQuery, { schema: schemaName, table: tableName });
|
|
312
|
+
if (existsResult.rows.length === 0) {
|
|
313
|
+
throw new Error(`Table '${schemaName}.${tableName}' not found. ` +
|
|
314
|
+
`Use sql-list-tables to see available tables.`);
|
|
315
|
+
}
|
|
316
|
+
// Get columns
|
|
317
|
+
const columnsQuery = `
|
|
318
|
+
SELECT
|
|
319
|
+
COLUMN_NAME as columnName,
|
|
320
|
+
DATA_TYPE as dataType,
|
|
321
|
+
CHARACTER_MAXIMUM_LENGTH as maxLength,
|
|
322
|
+
IS_NULLABLE as isNullable,
|
|
323
|
+
COLUMN_DEFAULT as defaultValue,
|
|
324
|
+
COLUMNPROPERTY(OBJECT_ID(@schema + '.' + @table), COLUMN_NAME, 'IsIdentity') as isIdentity
|
|
325
|
+
FROM INFORMATION_SCHEMA.COLUMNS
|
|
326
|
+
WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = @table
|
|
327
|
+
ORDER BY ORDINAL_POSITION
|
|
328
|
+
`;
|
|
329
|
+
// Get indexes
|
|
330
|
+
const indexesQuery = `
|
|
331
|
+
SELECT
|
|
332
|
+
i.name as indexName,
|
|
333
|
+
i.type_desc as indexType,
|
|
334
|
+
i.is_unique as isUnique,
|
|
335
|
+
i.is_primary_key as isPrimaryKey,
|
|
336
|
+
STRING_AGG(c.name, ', ') WITHIN GROUP (ORDER BY ic.key_ordinal) as columns
|
|
337
|
+
FROM sys.indexes i
|
|
338
|
+
INNER JOIN sys.tables t ON i.object_id = t.object_id
|
|
339
|
+
INNER JOIN sys.schemas s ON t.schema_id = s.schema_id
|
|
340
|
+
INNER JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
|
|
341
|
+
INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
|
|
342
|
+
WHERE s.name = @schema AND t.name = @table
|
|
343
|
+
GROUP BY i.name, i.type_desc, i.is_unique, i.is_primary_key
|
|
344
|
+
ORDER BY i.is_primary_key DESC, i.name
|
|
345
|
+
`;
|
|
346
|
+
// Get foreign keys
|
|
347
|
+
const foreignKeysQuery = `
|
|
348
|
+
SELECT
|
|
349
|
+
fk.name as foreignKeyName,
|
|
350
|
+
OBJECT_SCHEMA_NAME(fk.parent_object_id) as schemaName,
|
|
351
|
+
OBJECT_NAME(fk.parent_object_id) as tableName,
|
|
352
|
+
COL_NAME(fkc.parent_object_id, fkc.parent_column_id) as columnName,
|
|
353
|
+
OBJECT_SCHEMA_NAME(fk.referenced_object_id) as referencedSchema,
|
|
354
|
+
OBJECT_NAME(fk.referenced_object_id) as referencedTable,
|
|
355
|
+
COL_NAME(fkc.referenced_object_id, fkc.referenced_column_id) as referencedColumn
|
|
356
|
+
FROM sys.foreign_keys fk
|
|
357
|
+
INNER JOIN sys.foreign_key_columns fkc ON fk.object_id = fkc.constraint_object_id
|
|
358
|
+
WHERE OBJECT_SCHEMA_NAME(fk.parent_object_id) = @schema
|
|
359
|
+
AND OBJECT_NAME(fk.parent_object_id) = @table
|
|
360
|
+
ORDER BY fk.name
|
|
361
|
+
`;
|
|
362
|
+
// Query all schema information with graceful degradation
|
|
363
|
+
try {
|
|
364
|
+
const [columnsResult, indexesResult, foreignKeysResult] = await Promise.all([
|
|
365
|
+
this.executeQuery(columnsQuery, { schema: schemaName, table: tableName }),
|
|
366
|
+
this.executeQuery(indexesQuery, { schema: schemaName, table: tableName })
|
|
367
|
+
.catch(() => ({ rows: [], rowCount: 0, columns: [] })),
|
|
368
|
+
this.executeQuery(foreignKeysQuery, { schema: schemaName, table: tableName })
|
|
369
|
+
.catch(() => ({ rows: [], rowCount: 0, columns: [] })),
|
|
370
|
+
]);
|
|
371
|
+
return {
|
|
372
|
+
schemaName,
|
|
373
|
+
tableName,
|
|
374
|
+
columns: columnsResult.rows,
|
|
375
|
+
indexes: indexesResult.rows,
|
|
376
|
+
foreignKeys: foreignKeysResult.rows,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
throw new Error(`Failed to retrieve schema for '${schemaName}.${tableName}': ${this.sanitizeErrorMessage(error.message)}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Get the SQL definition for views, stored procedures, functions, or triggers
|
|
385
|
+
*/
|
|
386
|
+
async getObjectDefinition(schemaName, objectName, objectType) {
|
|
387
|
+
const query = `
|
|
388
|
+
SELECT
|
|
389
|
+
o.name as objectName,
|
|
390
|
+
s.name as schemaName,
|
|
391
|
+
o.type_desc as objectType,
|
|
392
|
+
o.create_date as createdDate,
|
|
393
|
+
o.modify_date as modifiedDate,
|
|
394
|
+
OBJECT_DEFINITION(o.object_id) as definition
|
|
395
|
+
FROM sys.objects o
|
|
396
|
+
INNER JOIN sys.schemas s ON o.schema_id = s.schema_id
|
|
397
|
+
WHERE s.name = @schema
|
|
398
|
+
AND o.name = @object
|
|
399
|
+
AND o.type_desc LIKE '%' + @type + '%'
|
|
400
|
+
`;
|
|
401
|
+
const result = await this.executeQuery(query, {
|
|
402
|
+
schema: schemaName,
|
|
403
|
+
object: objectName,
|
|
404
|
+
type: objectType,
|
|
405
|
+
});
|
|
406
|
+
if (result.rows.length === 0) {
|
|
407
|
+
throw new Error(`${objectType} '${schemaName}.${objectName}' not found. ` +
|
|
408
|
+
`Check the schema name, object name, and object type.`);
|
|
409
|
+
}
|
|
410
|
+
return result.rows[0];
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Execute a user-provided SELECT query with enhanced safety validation
|
|
414
|
+
*/
|
|
415
|
+
async executeSelectQuery(query) {
|
|
416
|
+
const timer = auditLogger.startTimer();
|
|
417
|
+
// Step 1: Remove comments (SQL and C-style)
|
|
418
|
+
let cleanQuery = query
|
|
419
|
+
.replace(/--.*$/gm, '') // Remove -- comments
|
|
420
|
+
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove /* */ comments
|
|
421
|
+
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
422
|
+
.trim()
|
|
423
|
+
.toLowerCase();
|
|
424
|
+
// Step 2: Validate SELECT query
|
|
425
|
+
if (!cleanQuery.startsWith('select')) {
|
|
426
|
+
const error = 'Only SELECT queries are allowed. Write operations (INSERT, UPDATE, DELETE, etc.) are not permitted.';
|
|
427
|
+
auditLogger.log({
|
|
428
|
+
operation: 'execute-select-query',
|
|
429
|
+
operationType: 'READ',
|
|
430
|
+
componentType: 'Query',
|
|
431
|
+
success: false,
|
|
432
|
+
error,
|
|
433
|
+
parameters: { query: query.substring(0, 500) },
|
|
434
|
+
executionTimeMs: timer()
|
|
435
|
+
});
|
|
436
|
+
throw new Error(error);
|
|
437
|
+
}
|
|
438
|
+
// Step 3: Check for dangerous keywords with word boundaries
|
|
439
|
+
const dangerousPatterns = [
|
|
440
|
+
{ pattern: /\b(insert|update|delete|merge)\b/i, name: 'write operations' },
|
|
441
|
+
{ pattern: /\b(drop|create|alter|truncate)\b/i, name: 'schema modifications' },
|
|
442
|
+
{ pattern: /\b(exec|execute|sp_executesql)\b/i, name: 'command execution' },
|
|
443
|
+
{ pattern: /\b(xp_|sp_)\w+/i, name: 'system stored procedures' },
|
|
444
|
+
{ pattern: /\b(grant|revoke|deny)\b/i, name: 'permission changes' },
|
|
445
|
+
{ pattern: /\binto\b/i, name: 'SELECT INTO' },
|
|
446
|
+
{ pattern: /\b(openquery|openrowset|opendatasource)\b/i, name: 'linked server queries' },
|
|
447
|
+
];
|
|
448
|
+
for (const { pattern, name } of dangerousPatterns) {
|
|
449
|
+
if (pattern.test(cleanQuery)) {
|
|
450
|
+
const error = `Query contains forbidden keyword or pattern (${name}). Only SELECT queries are allowed for investigation purposes.`;
|
|
451
|
+
auditLogger.log({
|
|
452
|
+
operation: 'execute-select-query',
|
|
453
|
+
operationType: 'READ',
|
|
454
|
+
componentType: 'Query',
|
|
455
|
+
success: false,
|
|
456
|
+
error,
|
|
457
|
+
parameters: { query: query.substring(0, 500) },
|
|
458
|
+
executionTimeMs: timer()
|
|
459
|
+
});
|
|
460
|
+
throw new Error(error);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
// Execute query with audit logging
|
|
464
|
+
try {
|
|
465
|
+
const result = await this.executeQuery(query);
|
|
466
|
+
auditLogger.log({
|
|
467
|
+
operation: 'execute-select-query',
|
|
468
|
+
operationType: 'READ',
|
|
469
|
+
componentType: 'Query',
|
|
470
|
+
parameters: {
|
|
471
|
+
query: query.substring(0, 500),
|
|
472
|
+
rowCount: result.rowCount,
|
|
473
|
+
truncated: result.truncated
|
|
474
|
+
},
|
|
475
|
+
success: true,
|
|
476
|
+
executionTimeMs: timer()
|
|
477
|
+
});
|
|
478
|
+
if (result.truncated) {
|
|
479
|
+
console.error(`Query results truncated. Returned ${result.rowCount} of potentially more rows. ` +
|
|
480
|
+
`Maximum: ${this.config.maxResultRows}. Add WHERE clause to filter results.`);
|
|
481
|
+
}
|
|
482
|
+
return result;
|
|
483
|
+
}
|
|
484
|
+
catch (error) {
|
|
485
|
+
auditLogger.log({
|
|
486
|
+
operation: 'execute-select-query',
|
|
487
|
+
operationType: 'READ',
|
|
488
|
+
componentType: 'Query',
|
|
489
|
+
success: false,
|
|
490
|
+
error: error instanceof Error ? error.message : String(error),
|
|
491
|
+
parameters: { query: query.substring(0, 500) },
|
|
492
|
+
executionTimeMs: timer()
|
|
493
|
+
});
|
|
494
|
+
throw error;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|