mssql-mcp 2.1.1 → 2.3.1
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 +21 -0
- package/README.md +161 -89
- package/dist/src/config.d.ts +39 -0
- package/dist/src/config.js +37 -0
- package/dist/src/constants.d.ts +15 -0
- package/dist/src/constants.js +15 -0
- package/dist/src/db/connection.d.ts +8 -0
- package/dist/src/db/connection.js +80 -0
- package/dist/src/db/query-builders.d.ts +3 -0
- package/dist/src/db/query-builders.js +58 -0
- package/dist/src/db/validators.d.ts +5 -0
- package/dist/src/db/validators.js +25 -0
- package/dist/src/index.js +26 -0
- package/dist/src/resources/connection.d.ts +2 -0
- package/dist/src/resources/connection.js +19 -0
- package/dist/src/resources/metadata.d.ts +2 -0
- package/dist/src/resources/metadata.js +58 -0
- package/dist/src/schemas/outputs.d.ts +153 -0
- package/dist/src/schemas/outputs.js +54 -0
- package/dist/src/server.d.ts +2 -0
- package/dist/src/server.js +27 -0
- package/dist/src/tools/connect.d.ts +2 -0
- package/dist/src/tools/connect.js +45 -0
- package/dist/src/tools/databases.d.ts +2 -0
- package/dist/src/tools/databases.js +53 -0
- package/dist/src/tools/procedure.d.ts +2 -0
- package/dist/src/tools/procedure.js +106 -0
- package/dist/src/tools/query.d.ts +2 -0
- package/dist/src/tools/query.js +92 -0
- package/dist/src/tools/schema.d.ts +2 -0
- package/dist/src/tools/schema.js +96 -0
- package/dist/src/tools/status.d.ts +2 -0
- package/dist/src/tools/status.js +17 -0
- package/dist/src/tools/table.d.ts +2 -0
- package/dist/src/tools/table.js +261 -0
- package/dist/src/transports/http.d.ts +3 -0
- package/dist/src/transports/http.js +54 -0
- package/dist/src/transports/stdio.d.ts +2 -0
- package/dist/src/transports/stdio.js +23 -0
- package/dist/src/types.d.ts +37 -0
- package/dist/src/types.js +1 -0
- package/dist/src/utils/errors.d.ts +19 -0
- package/dist/src/utils/errors.js +29 -0
- package/dist/src/utils/format.d.ts +6 -0
- package/dist/src/utils/format.js +27 -0
- package/dist/src/utils/markdown.d.ts +3 -0
- package/dist/src/utils/markdown.js +33 -0
- package/dist/src/utils/pagination.d.ts +3 -0
- package/dist/src/utils/pagination.js +18 -0
- package/dist/tests/unit/markdown.test.d.ts +1 -0
- package/dist/tests/unit/markdown.test.js +70 -0
- package/dist/tests/unit/query-builders.test.d.ts +1 -0
- package/dist/tests/unit/query-builders.test.js +63 -0
- package/dist/tests/unit/tool-contracts.test.d.ts +1 -0
- package/dist/tests/unit/tool-contracts.test.js +62 -0
- package/dist/tests/unit/validators.test.d.ts +1 -0
- package/dist/tests/unit/validators.test.js +51 -0
- package/package.json +10 -6
- package/dist/index.js +0 -648
- /package/dist/{index.d.ts → src/index.d.ts} +0 -0
package/dist/index.js
DELETED
|
@@ -1,648 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import sql from "mssql";
|
|
5
|
-
import { z } from "zod";
|
|
6
|
-
import dotenv from "dotenv";
|
|
7
|
-
// Load environment variables
|
|
8
|
-
dotenv.config();
|
|
9
|
-
// Database connection configuration schema with strict validation
|
|
10
|
-
const ConfigSchema = z.object({
|
|
11
|
-
server: z.string().min(1, "Server address is required"),
|
|
12
|
-
database: z.string().optional(),
|
|
13
|
-
user: z.string().optional(),
|
|
14
|
-
password: z.string().optional(),
|
|
15
|
-
port: z.number().int().min(1).max(65535).optional().default(1433),
|
|
16
|
-
encrypt: z.boolean().optional().default(true),
|
|
17
|
-
trustServerCertificate: z.boolean().optional().default(false),
|
|
18
|
-
connectionTimeout: z.number().int().min(1000).max(60000).optional().default(30000),
|
|
19
|
-
requestTimeout: z.number().int().min(1000).max(300000).optional().default(30000),
|
|
20
|
-
});
|
|
21
|
-
class MSSQLMCPServer {
|
|
22
|
-
server;
|
|
23
|
-
pool = null;
|
|
24
|
-
config = null;
|
|
25
|
-
constructor() {
|
|
26
|
-
this.server = new McpServer({
|
|
27
|
-
name: "mssql-mcp-server",
|
|
28
|
-
version: "2.1.1",
|
|
29
|
-
});
|
|
30
|
-
this.setupTools();
|
|
31
|
-
this.setupResources();
|
|
32
|
-
}
|
|
33
|
-
setupTools() {
|
|
34
|
-
// Tool: Connect to database with enhanced security validation (only uses environment variables)
|
|
35
|
-
this.server.registerTool("connect_database", {
|
|
36
|
-
title: "Connect Database",
|
|
37
|
-
description: "Connect to MS SQL Server database with security validation (uses only environment variables for security)",
|
|
38
|
-
inputSchema: {
|
|
39
|
-
// No parameters - only environment variables will be used for security
|
|
40
|
-
},
|
|
41
|
-
}, async () => {
|
|
42
|
-
try {
|
|
43
|
-
// SECURITY: Only use environment variables, ignore all user parameters
|
|
44
|
-
const config = ConfigSchema.parse({
|
|
45
|
-
server: process.env.DB_SERVER,
|
|
46
|
-
database: process.env.DB_DATABASE,
|
|
47
|
-
user: process.env.DB_USER,
|
|
48
|
-
password: process.env.DB_PASSWORD,
|
|
49
|
-
port: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 1433,
|
|
50
|
-
encrypt: process.env.DB_ENCRYPT !== 'false', // Default true for Azure SQL compatibility
|
|
51
|
-
trustServerCertificate: process.env.DB_TRUST_SERVER_CERTIFICATE === 'true',
|
|
52
|
-
connectionTimeout: process.env.DB_CONNECTION_TIMEOUT ? parseInt(process.env.DB_CONNECTION_TIMEOUT) : 30000,
|
|
53
|
-
requestTimeout: process.env.DB_REQUEST_TIMEOUT ? parseInt(process.env.DB_REQUEST_TIMEOUT) : 30000,
|
|
54
|
-
});
|
|
55
|
-
if (!config.server) {
|
|
56
|
-
throw new Error("Server is required. Provide it as parameter or set DB_SERVER environment variable.");
|
|
57
|
-
}
|
|
58
|
-
await this.connect(config);
|
|
59
|
-
return {
|
|
60
|
-
content: [
|
|
61
|
-
{
|
|
62
|
-
type: "text",
|
|
63
|
-
text: `✅ Successfully connected to SQL Server: ${config.server}${config.database ? ` (Database: ${config.database})` : ""}`,
|
|
64
|
-
},
|
|
65
|
-
],
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
catch (error) {
|
|
69
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
70
|
-
console.error("❌ Database connection failed:", errorMessage);
|
|
71
|
-
return {
|
|
72
|
-
content: [
|
|
73
|
-
{
|
|
74
|
-
type: "text",
|
|
75
|
-
text: `❌ Failed to connect: ${errorMessage}`,
|
|
76
|
-
},
|
|
77
|
-
],
|
|
78
|
-
isError: true,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
// Tool: Execute SQL query with enhanced security
|
|
83
|
-
this.server.registerTool("execute_query", {
|
|
84
|
-
title: "Execute Query",
|
|
85
|
-
description: "Execute a SQL query against the connected database with security validation",
|
|
86
|
-
inputSchema: {
|
|
87
|
-
query: z.string().min(1, "Query cannot be empty").describe("SQL query to execute"),
|
|
88
|
-
parameters: z.record(z.any()).optional().describe("Query parameters (key-value pairs) - always use parameters for user input"),
|
|
89
|
-
},
|
|
90
|
-
}, async ({ query, parameters }) => {
|
|
91
|
-
try {
|
|
92
|
-
if (!this.pool) {
|
|
93
|
-
throw new Error("No database connection. Please connect first using connect_database tool.");
|
|
94
|
-
}
|
|
95
|
-
const request = this.pool.request();
|
|
96
|
-
// Add parameters if provided (recommended for security)
|
|
97
|
-
if (parameters) {
|
|
98
|
-
for (const [key, value] of Object.entries(parameters)) {
|
|
99
|
-
request.input(key, value);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
const startTime = Date.now();
|
|
103
|
-
const result = await request.query(query);
|
|
104
|
-
const executionTime = Date.now() - startTime;
|
|
105
|
-
return {
|
|
106
|
-
content: [
|
|
107
|
-
{
|
|
108
|
-
type: "text",
|
|
109
|
-
text: JSON.stringify({
|
|
110
|
-
recordset: result.recordset,
|
|
111
|
-
rowsAffected: result.rowsAffected,
|
|
112
|
-
output: result.output,
|
|
113
|
-
executionTime: `${executionTime}ms`,
|
|
114
|
-
parametersUsed: parameters ? Object.keys(parameters).length : 0,
|
|
115
|
-
}, null, 2),
|
|
116
|
-
},
|
|
117
|
-
],
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
catch (error) {
|
|
121
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
122
|
-
console.error("❌ Query execution failed:", errorMessage);
|
|
123
|
-
return {
|
|
124
|
-
content: [
|
|
125
|
-
{
|
|
126
|
-
type: "text",
|
|
127
|
-
text: `❌ Query execution failed: ${errorMessage}`,
|
|
128
|
-
},
|
|
129
|
-
],
|
|
130
|
-
isError: true,
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
// Tool: Get database schema
|
|
135
|
-
this.server.registerTool("get_schema", {
|
|
136
|
-
title: "Get Schema",
|
|
137
|
-
description: "Get database schema information (tables, columns, etc.)",
|
|
138
|
-
inputSchema: {
|
|
139
|
-
objectType: z.enum(["tables", "views", "procedures", "functions", "all"]).optional().default("tables"),
|
|
140
|
-
schemaName: z.string().optional().describe("Specific schema name to filter"),
|
|
141
|
-
},
|
|
142
|
-
}, async ({ objectType, schemaName }) => {
|
|
143
|
-
try {
|
|
144
|
-
if (!this.pool) {
|
|
145
|
-
throw new Error("No database connection. Please connect first.");
|
|
146
|
-
}
|
|
147
|
-
let query = "";
|
|
148
|
-
if (objectType === "tables" || objectType === "all") {
|
|
149
|
-
query += `
|
|
150
|
-
SELECT
|
|
151
|
-
TABLE_SCHEMA,
|
|
152
|
-
TABLE_NAME,
|
|
153
|
-
TABLE_TYPE,
|
|
154
|
-
'table' as OBJECT_TYPE
|
|
155
|
-
FROM INFORMATION_SCHEMA.TABLES
|
|
156
|
-
${schemaName ? `WHERE TABLE_SCHEMA = '${schemaName}'` : ""}
|
|
157
|
-
`;
|
|
158
|
-
}
|
|
159
|
-
if (objectType === "views" || objectType === "all") {
|
|
160
|
-
if (query)
|
|
161
|
-
query += " UNION ALL ";
|
|
162
|
-
query += `
|
|
163
|
-
SELECT
|
|
164
|
-
TABLE_SCHEMA,
|
|
165
|
-
TABLE_NAME,
|
|
166
|
-
'VIEW' as TABLE_TYPE,
|
|
167
|
-
'view' as OBJECT_TYPE
|
|
168
|
-
FROM INFORMATION_SCHEMA.VIEWS
|
|
169
|
-
${schemaName ? `WHERE TABLE_SCHEMA = '${schemaName}'` : ""}
|
|
170
|
-
`;
|
|
171
|
-
}
|
|
172
|
-
if (objectType === "procedures" || objectType === "all") {
|
|
173
|
-
if (query)
|
|
174
|
-
query += " UNION ALL ";
|
|
175
|
-
query += `
|
|
176
|
-
SELECT
|
|
177
|
-
ROUTINE_SCHEMA as TABLE_SCHEMA,
|
|
178
|
-
ROUTINE_NAME as TABLE_NAME,
|
|
179
|
-
'PROCEDURE' as TABLE_TYPE,
|
|
180
|
-
'procedure' as OBJECT_TYPE
|
|
181
|
-
FROM INFORMATION_SCHEMA.ROUTINES
|
|
182
|
-
WHERE ROUTINE_TYPE = 'PROCEDURE'
|
|
183
|
-
${schemaName ? `AND ROUTINE_SCHEMA = '${schemaName}'` : ""}
|
|
184
|
-
`;
|
|
185
|
-
}
|
|
186
|
-
if (objectType === "functions" || objectType === "all") {
|
|
187
|
-
if (query)
|
|
188
|
-
query += " UNION ALL ";
|
|
189
|
-
query += `
|
|
190
|
-
SELECT
|
|
191
|
-
ROUTINE_SCHEMA as TABLE_SCHEMA,
|
|
192
|
-
ROUTINE_NAME as TABLE_NAME,
|
|
193
|
-
'FUNCTION' as TABLE_TYPE,
|
|
194
|
-
'function' as OBJECT_TYPE
|
|
195
|
-
FROM INFORMATION_SCHEMA.ROUTINES
|
|
196
|
-
WHERE ROUTINE_TYPE = 'FUNCTION'
|
|
197
|
-
${schemaName ? `AND ROUTINE_SCHEMA = '${schemaName}'` : ""}
|
|
198
|
-
`;
|
|
199
|
-
}
|
|
200
|
-
query += " ORDER BY TABLE_SCHEMA, TABLE_NAME";
|
|
201
|
-
const result = await this.pool.request().query(query);
|
|
202
|
-
return {
|
|
203
|
-
content: [
|
|
204
|
-
{
|
|
205
|
-
type: "text",
|
|
206
|
-
text: JSON.stringify(result.recordset, null, 2),
|
|
207
|
-
},
|
|
208
|
-
],
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
catch (error) {
|
|
212
|
-
return {
|
|
213
|
-
content: [
|
|
214
|
-
{
|
|
215
|
-
type: "text",
|
|
216
|
-
text: `Schema query failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
217
|
-
},
|
|
218
|
-
],
|
|
219
|
-
isError: true,
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
});
|
|
223
|
-
// Tool: Get table structure
|
|
224
|
-
this.server.registerTool("describe_table", {
|
|
225
|
-
title: "Describe Table",
|
|
226
|
-
description: "Get detailed structure of a specific table",
|
|
227
|
-
inputSchema: {
|
|
228
|
-
tableName: z.string().describe("Name of the table"),
|
|
229
|
-
schemaName: z.string().optional().default("dbo").describe("Schema name"),
|
|
230
|
-
},
|
|
231
|
-
}, async ({ tableName, schemaName }) => {
|
|
232
|
-
try {
|
|
233
|
-
if (!this.pool) {
|
|
234
|
-
throw new Error("No database connection. Please connect first.");
|
|
235
|
-
}
|
|
236
|
-
const query = `
|
|
237
|
-
SELECT
|
|
238
|
-
COLUMN_NAME,
|
|
239
|
-
DATA_TYPE,
|
|
240
|
-
CHARACTER_MAXIMUM_LENGTH,
|
|
241
|
-
NUMERIC_PRECISION,
|
|
242
|
-
NUMERIC_SCALE,
|
|
243
|
-
IS_NULLABLE,
|
|
244
|
-
COLUMN_DEFAULT,
|
|
245
|
-
ORDINAL_POSITION
|
|
246
|
-
FROM INFORMATION_SCHEMA.COLUMNS
|
|
247
|
-
WHERE TABLE_NAME = @tableName
|
|
248
|
-
AND TABLE_SCHEMA = @schemaName
|
|
249
|
-
ORDER BY ORDINAL_POSITION
|
|
250
|
-
`;
|
|
251
|
-
const result = await this.pool.request()
|
|
252
|
-
.input('tableName', sql.VarChar, tableName)
|
|
253
|
-
.input('schemaName', sql.VarChar, schemaName)
|
|
254
|
-
.query(query);
|
|
255
|
-
return {
|
|
256
|
-
content: [
|
|
257
|
-
{
|
|
258
|
-
type: "text",
|
|
259
|
-
text: JSON.stringify(result.recordset, null, 2),
|
|
260
|
-
},
|
|
261
|
-
],
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
catch (error) {
|
|
265
|
-
return {
|
|
266
|
-
content: [
|
|
267
|
-
{
|
|
268
|
-
type: "text",
|
|
269
|
-
text: `Table description failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
270
|
-
},
|
|
271
|
-
],
|
|
272
|
-
isError: true,
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
});
|
|
276
|
-
// Tool: Get enhanced connection status
|
|
277
|
-
this.server.registerTool("connection_status", {
|
|
278
|
-
title: "Connection Status",
|
|
279
|
-
description: "Check current database connection status with detailed information",
|
|
280
|
-
inputSchema: {},
|
|
281
|
-
}, async () => {
|
|
282
|
-
const isConnected = this.pool?.connected || false;
|
|
283
|
-
const status = {
|
|
284
|
-
connected: isConnected,
|
|
285
|
-
server: this.config?.server || "Not configured",
|
|
286
|
-
database: this.config?.database || "Not specified",
|
|
287
|
-
port: this.config?.port || "Not specified",
|
|
288
|
-
connectionTime: isConnected ? new Date().toISOString() : null,
|
|
289
|
-
securityFeatures: {
|
|
290
|
-
sqlInjectionProtection: "Enabled",
|
|
291
|
-
},
|
|
292
|
-
poolInfo: this.pool ? {
|
|
293
|
-
size: this.pool.size,
|
|
294
|
-
available: this.pool.available,
|
|
295
|
-
pending: this.pool.pending,
|
|
296
|
-
borrowed: this.pool.borrowed,
|
|
297
|
-
} : null,
|
|
298
|
-
};
|
|
299
|
-
return {
|
|
300
|
-
content: [
|
|
301
|
-
{
|
|
302
|
-
type: "text",
|
|
303
|
-
text: JSON.stringify(status, null, 2),
|
|
304
|
-
},
|
|
305
|
-
],
|
|
306
|
-
};
|
|
307
|
-
});
|
|
308
|
-
// Tool: Disconnect from database
|
|
309
|
-
this.server.registerTool("disconnect_database", {
|
|
310
|
-
title: "Disconnect Database",
|
|
311
|
-
description: "Disconnect from the current database",
|
|
312
|
-
inputSchema: {},
|
|
313
|
-
}, async () => {
|
|
314
|
-
try {
|
|
315
|
-
if (this.pool) {
|
|
316
|
-
await this.pool.close();
|
|
317
|
-
this.pool = null;
|
|
318
|
-
this.config = null;
|
|
319
|
-
}
|
|
320
|
-
return {
|
|
321
|
-
content: [
|
|
322
|
-
{
|
|
323
|
-
type: "text",
|
|
324
|
-
text: "Successfully disconnected from database",
|
|
325
|
-
},
|
|
326
|
-
],
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
catch (error) {
|
|
330
|
-
return {
|
|
331
|
-
content: [
|
|
332
|
-
{
|
|
333
|
-
type: "text",
|
|
334
|
-
text: `Disconnect failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
335
|
-
},
|
|
336
|
-
],
|
|
337
|
-
isError: true,
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
});
|
|
341
|
-
// Tool: Get table data with enhanced security and validation
|
|
342
|
-
this.server.registerTool("get_table_data", {
|
|
343
|
-
title: "Get Table Data",
|
|
344
|
-
description: "Get data from a specific table with optional filtering, pagination and input validation",
|
|
345
|
-
inputSchema: {
|
|
346
|
-
tableName: z.string().min(1).regex(/^[a-zA-Z0-9_]+$/, "Table name can only contain letters, numbers, and underscores").describe("Name of the table"),
|
|
347
|
-
schemaName: z.string().regex(/^[a-zA-Z0-9_]+$/, "Schema name can only contain letters, numbers, and underscores").optional().default("dbo").describe("Schema name"),
|
|
348
|
-
limit: z.number().int().min(1).max(10000).optional().default(100).describe("Maximum number of rows to return (1-10000)"),
|
|
349
|
-
offset: z.number().int().min(0).optional().default(0).describe("Number of rows to skip"),
|
|
350
|
-
whereClause: z.string().optional().describe("WHERE clause (without the WHERE keyword) - use parameters for values"),
|
|
351
|
-
orderBy: z.string().optional().describe("ORDER BY clause (without the ORDER BY keyword)"),
|
|
352
|
-
parameters: z.record(z.any()).optional().describe("Parameters for WHERE clause"),
|
|
353
|
-
},
|
|
354
|
-
}, async ({ tableName, schemaName, limit, offset, whereClause, orderBy, parameters }) => {
|
|
355
|
-
try {
|
|
356
|
-
if (!this.pool) {
|
|
357
|
-
throw new Error("No database connection. Please connect first.");
|
|
358
|
-
}
|
|
359
|
-
// Security: Validate table and schema names to prevent SQL injection
|
|
360
|
-
const tableNamePattern = /^[a-zA-Z0-9_]+$/;
|
|
361
|
-
if (!tableNamePattern.test(tableName)) {
|
|
362
|
-
throw new Error("Invalid table name. Only letters, numbers, and underscores are allowed.");
|
|
363
|
-
}
|
|
364
|
-
if (!tableNamePattern.test(schemaName)) {
|
|
365
|
-
throw new Error("Invalid schema name. Only letters, numbers, and underscores are allowed.");
|
|
366
|
-
}
|
|
367
|
-
// Build query using parameterized approach
|
|
368
|
-
let query = `SELECT * FROM [${schemaName}].[${tableName}]`;
|
|
369
|
-
const request = this.pool.request();
|
|
370
|
-
if (whereClause) {
|
|
371
|
-
// Add parameters for WHERE clause if provided
|
|
372
|
-
if (parameters) {
|
|
373
|
-
for (const [key, value] of Object.entries(parameters)) {
|
|
374
|
-
request.input(key, value);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
query += ` WHERE ${whereClause}`;
|
|
378
|
-
}
|
|
379
|
-
if (orderBy) {
|
|
380
|
-
// Validate ORDER BY clause for basic security
|
|
381
|
-
// Allow dotted identifiers, bracketed identifiers, commas, spaces and optional ASC/DESC per column
|
|
382
|
-
const orderByPattern = /^([\[\]a-zA-Z0-9_.]+(\s+(ASC|DESC))?)((\s*,\s*)[\[\]a-zA-Z0-9_.]+(\s+(ASC|DESC))?)*$/i;
|
|
383
|
-
if (!orderByPattern.test(orderBy)) {
|
|
384
|
-
throw new Error("Invalid ORDER BY clause. Only column names, commas, spaces, ASC, and DESC are allowed.");
|
|
385
|
-
}
|
|
386
|
-
query += ` ORDER BY ${orderBy}`;
|
|
387
|
-
}
|
|
388
|
-
else {
|
|
389
|
-
// Default ordering for pagination
|
|
390
|
-
query += ` ORDER BY (SELECT NULL)`;
|
|
391
|
-
}
|
|
392
|
-
query += ` OFFSET ${offset} ROWS FETCH NEXT ${limit} ROWS ONLY`;
|
|
393
|
-
const startTime = Date.now();
|
|
394
|
-
const result = await request.query(query);
|
|
395
|
-
const executionTime = Date.now() - startTime;
|
|
396
|
-
return {
|
|
397
|
-
content: [
|
|
398
|
-
{
|
|
399
|
-
type: "text",
|
|
400
|
-
text: JSON.stringify({
|
|
401
|
-
data: result.recordset,
|
|
402
|
-
metadata: {
|
|
403
|
-
rowCount: result.recordset.length,
|
|
404
|
-
offset: offset,
|
|
405
|
-
limit: limit,
|
|
406
|
-
executionTime: `${executionTime}ms`,
|
|
407
|
-
table: `${schemaName}.${tableName}`,
|
|
408
|
-
hasWhereClause: !!whereClause,
|
|
409
|
-
parametersUsed: parameters ? Object.keys(parameters).length : 0,
|
|
410
|
-
}
|
|
411
|
-
}, null, 2),
|
|
412
|
-
},
|
|
413
|
-
],
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
catch (error) {
|
|
417
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
418
|
-
console.error("❌ Get table data failed:", errorMessage);
|
|
419
|
-
return {
|
|
420
|
-
content: [
|
|
421
|
-
{
|
|
422
|
-
type: "text",
|
|
423
|
-
text: `❌ Get table data failed: ${errorMessage}`,
|
|
424
|
-
},
|
|
425
|
-
],
|
|
426
|
-
isError: true,
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
});
|
|
430
|
-
// Tool: Execute stored procedure
|
|
431
|
-
this.server.registerTool("execute_procedure", {
|
|
432
|
-
title: "Execute Procedure",
|
|
433
|
-
description: "Execute a stored procedure with parameters",
|
|
434
|
-
inputSchema: {
|
|
435
|
-
procedureName: z.string().describe("Name of the stored procedure"),
|
|
436
|
-
schemaName: z.string().optional().default("dbo").describe("Schema name"),
|
|
437
|
-
parameters: z.record(z.any()).optional().describe("Procedure parameters (key-value pairs)"),
|
|
438
|
-
},
|
|
439
|
-
}, async ({ procedureName, schemaName, parameters }) => {
|
|
440
|
-
try {
|
|
441
|
-
if (!this.pool) {
|
|
442
|
-
throw new Error("No database connection. Please connect first.");
|
|
443
|
-
}
|
|
444
|
-
const request = this.pool.request();
|
|
445
|
-
// Add parameters if provided
|
|
446
|
-
if (parameters) {
|
|
447
|
-
for (const [key, value] of Object.entries(parameters)) {
|
|
448
|
-
request.input(key, value);
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
const result = await request.execute(`[${schemaName}].[${procedureName}]`);
|
|
452
|
-
return {
|
|
453
|
-
content: [
|
|
454
|
-
{
|
|
455
|
-
type: "text",
|
|
456
|
-
text: JSON.stringify({
|
|
457
|
-
recordsets: result.recordsets,
|
|
458
|
-
rowsAffected: result.rowsAffected,
|
|
459
|
-
output: result.output,
|
|
460
|
-
returnValue: result.returnValue,
|
|
461
|
-
}, null, 2),
|
|
462
|
-
},
|
|
463
|
-
],
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
catch (error) {
|
|
467
|
-
return {
|
|
468
|
-
content: [
|
|
469
|
-
{
|
|
470
|
-
type: "text",
|
|
471
|
-
text: `Procedure execution failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
472
|
-
},
|
|
473
|
-
],
|
|
474
|
-
isError: true,
|
|
475
|
-
};
|
|
476
|
-
}
|
|
477
|
-
});
|
|
478
|
-
// Tool: Get database list
|
|
479
|
-
this.server.registerTool("list_databases", {
|
|
480
|
-
title: "List Databases",
|
|
481
|
-
description: "List all databases on the connected SQL Server instance",
|
|
482
|
-
inputSchema: {},
|
|
483
|
-
}, async () => {
|
|
484
|
-
try {
|
|
485
|
-
if (!this.pool) {
|
|
486
|
-
throw new Error("No database connection. Please connect first.");
|
|
487
|
-
}
|
|
488
|
-
const query = `
|
|
489
|
-
SELECT
|
|
490
|
-
name,
|
|
491
|
-
database_id,
|
|
492
|
-
create_date,
|
|
493
|
-
collation_name,
|
|
494
|
-
state_desc,
|
|
495
|
-
user_access_desc,
|
|
496
|
-
is_read_only,
|
|
497
|
-
is_auto_close_on,
|
|
498
|
-
is_auto_shrink_on,
|
|
499
|
-
recovery_model_desc
|
|
500
|
-
FROM sys.databases
|
|
501
|
-
ORDER BY name
|
|
502
|
-
`;
|
|
503
|
-
const result = await this.pool.request().query(query);
|
|
504
|
-
return {
|
|
505
|
-
content: [
|
|
506
|
-
{
|
|
507
|
-
type: "text",
|
|
508
|
-
text: JSON.stringify(result.recordset, null, 2),
|
|
509
|
-
},
|
|
510
|
-
],
|
|
511
|
-
};
|
|
512
|
-
}
|
|
513
|
-
catch (error) {
|
|
514
|
-
return {
|
|
515
|
-
content: [
|
|
516
|
-
{
|
|
517
|
-
type: "text",
|
|
518
|
-
text: `List databases failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
519
|
-
},
|
|
520
|
-
],
|
|
521
|
-
isError: true,
|
|
522
|
-
};
|
|
523
|
-
}
|
|
524
|
-
});
|
|
525
|
-
}
|
|
526
|
-
setupResources() {
|
|
527
|
-
// Resource: Current connection info
|
|
528
|
-
this.server.registerResource("connection-info", "mssql://connection/info", {
|
|
529
|
-
title: "Connection Info",
|
|
530
|
-
description: "Current MSSQL connection information",
|
|
531
|
-
mimeType: "application/json",
|
|
532
|
-
}, async () => {
|
|
533
|
-
const info = {
|
|
534
|
-
connected: this.pool?.connected || false,
|
|
535
|
-
config: this.config ? {
|
|
536
|
-
server: this.config.server,
|
|
537
|
-
database: this.config.database,
|
|
538
|
-
port: this.config.port,
|
|
539
|
-
} : null,
|
|
540
|
-
};
|
|
541
|
-
return {
|
|
542
|
-
contents: [
|
|
543
|
-
{
|
|
544
|
-
uri: "mssql://connection/info",
|
|
545
|
-
text: JSON.stringify(info, null, 2),
|
|
546
|
-
mimeType: "application/json",
|
|
547
|
-
},
|
|
548
|
-
],
|
|
549
|
-
};
|
|
550
|
-
});
|
|
551
|
-
}
|
|
552
|
-
async connect(config) {
|
|
553
|
-
try {
|
|
554
|
-
// Close existing connection if any
|
|
555
|
-
if (this.pool) {
|
|
556
|
-
console.error("Closing existing connection...");
|
|
557
|
-
await this.pool.close();
|
|
558
|
-
}
|
|
559
|
-
console.error(`Connecting to ${config.server}:${config.port}`);
|
|
560
|
-
// Create new connection pool with enhanced security settings
|
|
561
|
-
this.pool = new sql.ConnectionPool({
|
|
562
|
-
server: config.server,
|
|
563
|
-
database: config.database,
|
|
564
|
-
user: config.user,
|
|
565
|
-
password: config.password,
|
|
566
|
-
port: config.port,
|
|
567
|
-
options: {
|
|
568
|
-
encrypt: config.encrypt,
|
|
569
|
-
trustServerCertificate: config.trustServerCertificate,
|
|
570
|
-
enableArithAbort: true,
|
|
571
|
-
},
|
|
572
|
-
connectionTimeout: config.connectionTimeout,
|
|
573
|
-
requestTimeout: config.requestTimeout,
|
|
574
|
-
// Connection pool settings for better resource management
|
|
575
|
-
pool: {
|
|
576
|
-
max: 10,
|
|
577
|
-
min: 0,
|
|
578
|
-
idleTimeoutMillis: 30000,
|
|
579
|
-
},
|
|
580
|
-
});
|
|
581
|
-
// Set up event handlers for better monitoring
|
|
582
|
-
this.pool.on('connect', () => {
|
|
583
|
-
console.error('Database connected');
|
|
584
|
-
});
|
|
585
|
-
this.pool.on('error', (err) => {
|
|
586
|
-
console.error('Database error:', err);
|
|
587
|
-
});
|
|
588
|
-
await this.pool.connect();
|
|
589
|
-
this.config = config;
|
|
590
|
-
console.error(`Connected to ${config.server}${config.database ? `/${config.database}` : ''}`);
|
|
591
|
-
}
|
|
592
|
-
catch (error) {
|
|
593
|
-
console.error("Connection failed:", error);
|
|
594
|
-
if (this.pool) {
|
|
595
|
-
try {
|
|
596
|
-
await this.pool.close();
|
|
597
|
-
}
|
|
598
|
-
catch (closeError) {
|
|
599
|
-
console.error("Error closing failed connection:", closeError);
|
|
600
|
-
}
|
|
601
|
-
this.pool = null;
|
|
602
|
-
}
|
|
603
|
-
this.config = null;
|
|
604
|
-
throw error;
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
async run() {
|
|
608
|
-
const transport = new StdioServerTransport();
|
|
609
|
-
// Enhanced graceful shutdown handling
|
|
610
|
-
const shutdown = async (signal) => {
|
|
611
|
-
console.error(`\nShutting down (${signal})...`);
|
|
612
|
-
try {
|
|
613
|
-
if (this.pool) {
|
|
614
|
-
console.error("Closing database connection...");
|
|
615
|
-
await this.pool.close();
|
|
616
|
-
console.error("Database connection closed");
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
catch (error) {
|
|
620
|
-
console.error("Shutdown error:", error);
|
|
621
|
-
}
|
|
622
|
-
console.error("Server stopped");
|
|
623
|
-
process.exit(0);
|
|
624
|
-
};
|
|
625
|
-
// Handle various shutdown signals
|
|
626
|
-
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
627
|
-
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
628
|
-
process.on('SIGUSR2', () => shutdown('SIGUSR2')); // For nodemon
|
|
629
|
-
// Handle uncaught exceptions
|
|
630
|
-
process.on('uncaughtException', (error) => {
|
|
631
|
-
console.error('Uncaught Exception:', error);
|
|
632
|
-
shutdown('uncaughtException');
|
|
633
|
-
});
|
|
634
|
-
process.on('unhandledRejection', (reason, promise) => {
|
|
635
|
-
console.error('Unhandled Rejection:', reason);
|
|
636
|
-
shutdown('unhandledRejection');
|
|
637
|
-
});
|
|
638
|
-
console.error("MSSQL MCP Server v2.1.1 starting...");
|
|
639
|
-
await this.server.connect(transport);
|
|
640
|
-
console.error("Server ready");
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
// Start the server
|
|
644
|
-
const server = new MSSQLMCPServer();
|
|
645
|
-
server.run().catch((error) => {
|
|
646
|
-
console.error("Server failed to start:", error);
|
|
647
|
-
process.exit(1);
|
|
648
|
-
});
|
|
File without changes
|