@kingsnow129/database-mcp 0.4.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/.env.example ADDED
@@ -0,0 +1,23 @@
1
+ DB_ENGINE=sqlserver
2
+ DB_CONNECTION_STRING=
3
+ DB_HOST=localhost
4
+ DB_SERVER=localhost
5
+ DB_PORT=1433
6
+ DB_NAME=master
7
+ DB_USER=sa
8
+ DB_PASSWORD=your_password
9
+ DB_SSL=false
10
+ DB_ENCRYPT=true
11
+ DB_TRUST_SERVER_CERT=true
12
+ DB_CONN_TIMEOUT_MS=15000
13
+ DB_REQUEST_TIMEOUT_MS=15000
14
+ DB_POOL_MAX=10
15
+ DB_POOL_MIN=0
16
+ DB_POOL_IDLE_TIMEOUT_MS=30000
17
+ DB_READ_ONLY=true
18
+ DB_MAX_ROWS=200
19
+ DB_ALIAS=
20
+ DB_DEFAULT_ALIAS=local-sqlserver
21
+ DB_CURRENT_SERVER=
22
+ DB_CURRENT_DATABASE=
23
+ DB_PROFILES_FILE=
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kingsnow129
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.
package/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # Database MCP
2
+
3
+ Database MCP is an MCP server for SQL Server, PostgreSQL, and MySQL.
4
+
5
+ It provides tools for:
6
+ - `connect`
7
+ - `health_check`
8
+ - `list_schemas`
9
+ - `list_tables`
10
+ - `describe_table`
11
+ - `query` (single SELECT only)
12
+
13
+ ## Release
14
+
15
+ Current release:
16
+ - NPM package: `@kingsnow129/database-mcp@0.4.0`
17
+ - MCP server name: `database-mcp`
18
+ - VSIX helper: `database-mcp-helper@0.4.0`
19
+
20
+ ## What Is New In 0.4.0
21
+
22
+ - Multi-database profile model (`servers` + `databases`) is now the primary config.
23
+ - Automatic profile resolution during `connect`:
24
+ - by `alias`
25
+ - by `serverName`
26
+ - by `host`
27
+ - fallback to `currentServer`, `defaultServer`, then legacy alias
28
+ - User install directory moved to:
29
+ - `${userHome}/.mcp-servers/database-mcp`
30
+ - VS Code helper command set simplified and includes:
31
+ - `Database MCP: Manage Databases`
32
+ - `Database MCP: Open MCP Server List`
33
+ - `Database MCP: Uninstall (User)`
34
+ - Legacy `sqlserverMcp` key is still written for compatibility.
35
+
36
+ ## Quick Start (MCP Config)
37
+
38
+ Example user/workspace MCP config:
39
+
40
+ ```json
41
+ {
42
+ "servers": {
43
+ "databaseMcp": {
44
+ "type": "stdio",
45
+ "command": "node",
46
+ "args": [
47
+ "${userHome}/.mcp-servers/database-mcp/node_modules/@kingsnow129/database-mcp/dist/server.js",
48
+ "--profilesFile",
49
+ "${userHome}/.mcp-servers/database-mcp/profiles.json"
50
+ ]
51
+ }
52
+ }
53
+ }
54
+ ```
55
+
56
+ You can also run directly with NPX:
57
+
58
+ ```json
59
+ {
60
+ "mcpServers": {
61
+ "database-mcp": {
62
+ "command": "npx",
63
+ "args": ["-y", "@kingsnow129/database-mcp"]
64
+ }
65
+ }
66
+ }
67
+ ```
68
+
69
+ ## User-Level Install (Windows)
70
+
71
+ Run:
72
+
73
+ ```powershell
74
+ powershell -ExecutionPolicy Bypass -File .\scripts\install-user.ps1
75
+ ```
76
+
77
+ This installs to `${HOME}\\.mcp-servers\\database-mcp` and updates `%APPDATA%\\Code\\User\\mcp.json`.
78
+
79
+ ## Profiles Format
80
+
81
+ `profiles.json` now uses server-level config with database entries:
82
+
83
+ ```json
84
+ {
85
+ "defaultServer": "local-server",
86
+ "currentServer": "local-server",
87
+ "currentDatabase": "master",
88
+ "servers": {
89
+ "local-server": {
90
+ "engine": "sqlserver",
91
+ "host": "localhost",
92
+ "port": 1433,
93
+ "integratedAuth": false,
94
+ "user": "sa",
95
+ "password": "",
96
+ "encrypt": true,
97
+ "trustServerCertificate": true,
98
+ "databases": [
99
+ { "name": "master", "readOnly": true, "maxRows": 200 }
100
+ ]
101
+ }
102
+ }
103
+ }
104
+ ```
105
+
106
+ ## VSIX Helper Extension
107
+
108
+ Build and install locally:
109
+
110
+ ```bash
111
+ cd vscode-extension
112
+ npm install
113
+ npm run package
114
+ code --install-extension database-mcp-helper-0.4.0.vsix --force
115
+ ```
116
+
117
+ ## Safety Defaults
118
+
119
+ - Read-only mode defaults to `true`
120
+ - Query tool only accepts one SELECT statement
121
+ - Semicolons are blocked
122
+ - Returned rows are capped (default `200`)
123
+
124
+ ## Development
125
+
126
+ ```bash
127
+ npm install
128
+ npm run build
129
+ npm run dev
130
+ ```
131
+
132
+ ## Publish
133
+
134
+ NPM publish flow:
135
+
136
+ ```bash
137
+ npm run build
138
+ npm publish --access public
139
+ ```
140
+
141
+ VSIX package flow:
142
+
143
+ ```bash
144
+ cd vscode-extension
145
+ npm run package
146
+ ```
package/dist/server.js ADDED
@@ -0,0 +1,1098 @@
1
+ #!/usr/bin/env node
2
+ import {createRequire} from "module";
3
+ const _require=createRequire(import.meta.url);
4
+ const {version: SERVER_VERSION}=_require("../package.json");
5
+ import dotenv from "dotenv";
6
+ import sql from "mssql";
7
+ import pg from "pg";
8
+ import mysql from "mysql2/promise";
9
+ import fs from "node:fs";
10
+ import os from "node:os";
11
+ import path from "node:path";
12
+ import {Server} from "@modelcontextprotocol/sdk/server/index.js";
13
+ import {StdioServerTransport} from "@modelcontextprotocol/sdk/server/stdio.js";
14
+ import {CallToolRequestSchema,ListToolsRequestSchema} from "@modelcontextprotocol/sdk/types.js";
15
+
16
+ dotenv.config();
17
+
18
+ const {Pool: PostgresPool}=pg;
19
+
20
+ const SUPPORTED_ENGINES=new Set(["sqlserver","postgres","mysql"]);
21
+
22
+ const CLI_OPTION_MAP={
23
+ alias: "alias",
24
+ serverName: "serverName",
25
+ defaultAlias: "defaultAlias",
26
+ profilesFile: "profilesFile",
27
+ currentServer: "currentServer",
28
+ currentDatabase: "currentDatabase",
29
+ engine: "engine",
30
+ host: "host",
31
+ connectionString: "connectionString",
32
+ server: "server",
33
+ port: "port",
34
+ database: "database",
35
+ user: "user",
36
+ password: "password",
37
+ encrypt: "encrypt",
38
+ ssl: "ssl",
39
+ trustServerCertificate: "trustServerCertificate",
40
+ readOnly: "readOnly",
41
+ maxRows: "maxRows"
42
+ };
43
+
44
+ const TOOL_NAMES={
45
+ CONNECT: "connect",
46
+ HEALTH_CHECK: "health_check",
47
+ LIST_SCHEMAS: "list_schemas",
48
+ LIST_TABLES: "list_tables",
49
+ DESCRIBE_TABLE: "describe_table",
50
+ QUERY: "query"
51
+ };
52
+
53
+ let connection=null;
54
+ let runtimeConfig=null;
55
+ let runtimeSettings={
56
+ readOnly: true,
57
+ maxRows: 200
58
+ };
59
+
60
+ function parseCliArgs(argv) {
61
+ const parsed={};
62
+
63
+ for(let index=0;index<argv.length;index+=1) {
64
+ const token=argv[index];
65
+ if(!String(token).startsWith("--")) {
66
+ continue;
67
+ }
68
+
69
+ const rawToken=String(token).slice(2);
70
+ if(rawToken==="help") {
71
+ console.error("Supported flags: --alias --serverName --defaultAlias --profilesFile --currentServer --currentDatabase --engine --host --connectionString --server --port --database --user --password --encrypt --ssl --trustServerCertificate --readOnly --maxRows");
72
+ process.exit(0);
73
+ }
74
+
75
+ const equalsIndex=rawToken.indexOf("=");
76
+ const rawName=equalsIndex===-1? rawToken:rawToken.slice(0,equalsIndex);
77
+ const mappedName=CLI_OPTION_MAP[rawName];
78
+ if(!mappedName) {
79
+ continue;
80
+ }
81
+
82
+ let value=equalsIndex===-1? undefined:rawToken.slice(equalsIndex+1);
83
+ if(value===undefined) {
84
+ const nextToken=argv[index+1];
85
+ if(nextToken!==undefined&&!String(nextToken).startsWith("--")) {
86
+ value=String(nextToken);
87
+ index+=1;
88
+ } else {
89
+ value="true";
90
+ }
91
+ }
92
+
93
+ parsed[mappedName]=value;
94
+ }
95
+
96
+ return parsed;
97
+ }
98
+
99
+ const cliOptions=parseCliArgs(process.argv.slice(2));
100
+
101
+ function boolFromEnv(value,fallback) {
102
+ if(value===undefined) return fallback;
103
+ return String(value).toLowerCase()==="true";
104
+ }
105
+
106
+ function intFromEnv(value,fallback) {
107
+ const parsed=Number.parseInt(String(value??""),10);
108
+ return Number.isFinite(parsed)? parsed:fallback;
109
+ }
110
+
111
+ function firstDefined(...values) {
112
+ return values.find((value) => value!==undefined);
113
+ }
114
+
115
+ function normalizeEngine(input) {
116
+ const normalized=String(input??"sqlserver").trim().toLowerCase();
117
+ if(!SUPPORTED_ENGINES.has(normalized)) {
118
+ throw new Error(`Unsupported DB engine: ${input}. Supported: sqlserver, postgres, mysql.`);
119
+ }
120
+ return normalized;
121
+ }
122
+
123
+ function getProfilesFilePath() {
124
+ return path.resolve(
125
+ firstDefined(
126
+ cliOptions.profilesFile,
127
+ process.env.DB_PROFILES_FILE,
128
+ path.join(os.homedir(),".mcp-servers","database-mcp","profiles.json")
129
+ )
130
+ );
131
+ }
132
+
133
+ function loadProfiles() {
134
+ const profilesPath=getProfilesFilePath();
135
+ if(!fs.existsSync(profilesPath)) {
136
+ return {
137
+ defaultServer: undefined,
138
+ currentServer: undefined,
139
+ currentDatabase: undefined,
140
+ servers: {},
141
+ defaultAlias: undefined,
142
+ aliases: {}
143
+ };
144
+ }
145
+
146
+ const raw=fs.readFileSync(profilesPath,"utf8").trim();
147
+ if(!raw) {
148
+ return {
149
+ defaultServer: undefined,
150
+ currentServer: undefined,
151
+ currentDatabase: undefined,
152
+ servers: {},
153
+ defaultAlias: undefined,
154
+ aliases: {}
155
+ };
156
+ }
157
+
158
+ const parsed=JSON.parse(raw);
159
+ if(!parsed||typeof parsed!=="object") {
160
+ return {
161
+ defaultServer: undefined,
162
+ currentServer: undefined,
163
+ currentDatabase: undefined,
164
+ servers: {},
165
+ defaultAlias: undefined,
166
+ aliases: {}
167
+ };
168
+ }
169
+
170
+ const servers=parsed.servers&&typeof parsed.servers==="object"? parsed.servers:{};
171
+ const defaultServer=typeof parsed.defaultServer==="string"&&parsed.defaultServer.trim().length>0
172
+ ? parsed.defaultServer.trim()
173
+ : undefined;
174
+ const currentServer=typeof parsed.currentServer==="string"&&parsed.currentServer.trim().length>0
175
+ ? parsed.currentServer.trim()
176
+ : undefined;
177
+ const currentDatabase=typeof parsed.currentDatabase==="string"&&parsed.currentDatabase.trim().length>0
178
+ ? parsed.currentDatabase.trim()
179
+ : undefined;
180
+
181
+ const aliases=parsed.aliases&&typeof parsed.aliases==="object"? parsed.aliases:{};
182
+ const defaultAlias=typeof parsed.defaultAlias==="string"&&parsed.defaultAlias.trim().length>0
183
+ ? parsed.defaultAlias.trim()
184
+ : undefined;
185
+
186
+ return {defaultServer,currentServer,currentDatabase,servers,defaultAlias,aliases};
187
+ }
188
+
189
+ function normalizeHostForCompare(value) {
190
+ return String(value??"").trim().toLowerCase();
191
+ }
192
+
193
+ function resolveSavedProfile(overrides={}) {
194
+ const profiles=loadProfiles();
195
+ const {servers,aliases}=profiles;
196
+
197
+ const requestedAlias=firstDefined(overrides.alias,cliOptions.alias,process.env.DB_ALIAS);
198
+ const requestedServerName=firstDefined(
199
+ overrides.serverName,
200
+ cliOptions.serverName,
201
+ overrides.currentServer,
202
+ cliOptions.currentServer,
203
+ process.env.DB_CURRENT_SERVER
204
+ );
205
+ const requestedHost=firstDefined(overrides.host,overrides.server,cliOptions.host,cliOptions.server,process.env.DB_HOST,process.env.DB_SERVER);
206
+
207
+ let source="none";
208
+ let aliasName;
209
+ let serverName;
210
+ let serverProfile;
211
+
212
+ if(requestedAlias) {
213
+ const aliasKey=String(requestedAlias).trim();
214
+ if(aliases[aliasKey]&&typeof aliases[aliasKey]==="object") {
215
+ source="alias";
216
+ aliasName=aliasKey;
217
+ serverProfile=aliases[aliasKey];
218
+ } else if(servers[aliasKey]&&typeof servers[aliasKey]==="object") {
219
+ source="serverName";
220
+ serverName=aliasKey;
221
+ serverProfile=servers[aliasKey];
222
+ }
223
+ }
224
+
225
+ if(!serverProfile&&requestedServerName) {
226
+ const key=String(requestedServerName).trim();
227
+ if(servers[key]&&typeof servers[key]==="object") {
228
+ source="serverName";
229
+ serverName=key;
230
+ serverProfile=servers[key];
231
+ }
232
+ }
233
+
234
+ if(!serverProfile&&requestedHost) {
235
+ const wanted=normalizeHostForCompare(requestedHost);
236
+ const found=Object.entries(servers).find(([,cfg]) => {
237
+ const hostA=normalizeHostForCompare(cfg?.host);
238
+ const hostB=normalizeHostForCompare(cfg?.server);
239
+ return wanted===hostA||wanted===hostB;
240
+ });
241
+ if(found) {
242
+ source="host";
243
+ serverName=found[0];
244
+ serverProfile=found[1];
245
+ }
246
+ }
247
+
248
+ if(!serverProfile&&profiles.currentServer&&servers[profiles.currentServer]) {
249
+ source="currentServer";
250
+ serverName=profiles.currentServer;
251
+ serverProfile=servers[profiles.currentServer];
252
+ }
253
+
254
+ if(!serverProfile&&profiles.defaultServer&&servers[profiles.defaultServer]) {
255
+ source="defaultServer";
256
+ serverName=profiles.defaultServer;
257
+ serverProfile=servers[profiles.defaultServer];
258
+ }
259
+
260
+ if(!serverProfile) {
261
+ const fallbackAlias=resolveAlias(overrides);
262
+ if(fallbackAlias&&aliases[fallbackAlias]&&typeof aliases[fallbackAlias]==="object") {
263
+ source="defaultAlias";
264
+ aliasName=fallbackAlias;
265
+ serverProfile=aliases[fallbackAlias];
266
+ }
267
+ }
268
+
269
+ let dbConfig;
270
+ const requestedDb=firstDefined(
271
+ overrides.database,
272
+ cliOptions.database,
273
+ profiles.currentDatabase,
274
+ process.env.DB_NAME,
275
+ process.env.DB_DATABASE
276
+ );
277
+
278
+ if(serverProfile?.databases&&Array.isArray(serverProfile.databases)) {
279
+ if(requestedDb) {
280
+ dbConfig=serverProfile.databases.find((db) => db?.name===requestedDb);
281
+ }
282
+ if(!dbConfig&&serverProfile.databases.length>0) {
283
+ dbConfig=serverProfile.databases[0];
284
+ }
285
+ }
286
+
287
+ return {
288
+ source,
289
+ aliasName,
290
+ serverName,
291
+ serverProfile,
292
+ dbConfig
293
+ };
294
+ }
295
+
296
+ function resolveAlias(overrides={}) {
297
+ const explicitAlias=firstDefined(overrides.alias,cliOptions.alias,process.env.DB_ALIAS);
298
+ if(explicitAlias) {
299
+ return String(explicitAlias).trim();
300
+ }
301
+
302
+ const defaultFromCliOrEnv=firstDefined(cliOptions.defaultAlias,process.env.DB_DEFAULT_ALIAS);
303
+ if(defaultFromCliOrEnv) {
304
+ return String(defaultFromCliOrEnv).trim();
305
+ }
306
+
307
+ return loadProfiles().defaultAlias;
308
+ }
309
+
310
+ function getAliasProfile(alias) {
311
+ if(!alias) {
312
+ return undefined;
313
+ }
314
+
315
+ const {aliases}=loadProfiles();
316
+ const profile=aliases[alias];
317
+ if(!profile||typeof profile!=="object") {
318
+ throw new Error(`Alias '${alias}' not found in ${getProfilesFilePath()}`);
319
+ }
320
+
321
+ return profile;
322
+ }
323
+
324
+ function getReadOnlyDefault() {
325
+ return boolFromEnv(firstDefined(cliOptions.readOnly,process.env.DB_READ_ONLY),true);
326
+ }
327
+
328
+ function getMaxRowsDefault() {
329
+ return intFromEnv(firstDefined(cliOptions.maxRows,process.env.DB_MAX_ROWS),200);
330
+ }
331
+
332
+ function buildConfig(overrides={}) {
333
+ const resolved=resolveSavedProfile(overrides);
334
+ const {serverProfile,dbConfig}=resolved;
335
+
336
+ const engine=normalizeEngine(
337
+ firstDefined(
338
+ overrides.engine,
339
+ cliOptions.engine,
340
+ serverProfile?.engine,
341
+ process.env.DB_ENGINE,
342
+ process.env.DB_TYPE,
343
+ "sqlserver"
344
+ )
345
+ );
346
+
347
+ const connectionString=firstDefined(
348
+ overrides.connectionString,
349
+ cliOptions.connectionString,
350
+ serverProfile?.connectionString,
351
+ process.env.DB_CONNECTION_STRING,
352
+ process.env.DATABASE_URL
353
+ );
354
+
355
+ const host=firstDefined(
356
+ overrides.host,
357
+ overrides.server,
358
+ cliOptions.host,
359
+ cliOptions.server,
360
+ serverProfile?.host,
361
+ serverProfile?.server,
362
+ process.env.DB_HOST,
363
+ process.env.DB_SERVER
364
+ );
365
+
366
+ const port=intFromEnv(
367
+ firstDefined(overrides.port,cliOptions.port,serverProfile?.port,process.env.DB_PORT),
368
+ engine==="sqlserver"? 1433:(engine==="postgres"? 5432:3306)
369
+ );
370
+
371
+ const database=firstDefined(
372
+ overrides.database,
373
+ cliOptions.database,
374
+ dbConfig?.name,
375
+ serverProfile?.database,
376
+ process.env.DB_NAME,
377
+ process.env.DB_DATABASE
378
+ );
379
+
380
+ const user=firstDefined(overrides.user,cliOptions.user,serverProfile?.user,process.env.DB_USER);
381
+ const password=firstDefined(
382
+ overrides.password,
383
+ cliOptions.password,
384
+ serverProfile?.password,
385
+ process.env.DB_PASSWORD
386
+ );
387
+
388
+ const integratedAuth=boolFromEnv(
389
+ firstDefined(overrides.integratedAuth,serverProfile?.integratedAuth,process.env.DB_INTEGRATED_AUTH),
390
+ false
391
+ );
392
+
393
+ const config={
394
+ profileSource: resolved.source,
395
+ alias: resolved.aliasName,
396
+ serverName: resolved.serverName,
397
+ engine,
398
+ host,
399
+ port,
400
+ database,
401
+ integratedAuth,
402
+ user: integratedAuth? "":user,
403
+ password: integratedAuth? "":password,
404
+ connectionString,
405
+ readOnly: boolFromEnv(firstDefined(overrides.readOnly,dbConfig?.readOnly,serverProfile?.readOnly),getReadOnlyDefault()),
406
+ maxRows: intFromEnv(firstDefined(overrides.maxRows,dbConfig?.maxRows,serverProfile?.maxRows),getMaxRowsDefault()),
407
+ trustServerCertificate: boolFromEnv(
408
+ firstDefined(
409
+ overrides.trustServerCertificate,
410
+ cliOptions.trustServerCertificate,
411
+ serverProfile?.trustServerCertificate,
412
+ process.env.DB_TRUST_SERVER_CERT
413
+ ),
414
+ engine==="sqlserver"
415
+ ),
416
+ encrypt: boolFromEnv(
417
+ firstDefined(overrides.encrypt,cliOptions.encrypt,serverProfile?.encrypt,process.env.DB_ENCRYPT),
418
+ engine==="sqlserver"
419
+ ),
420
+ ssl: boolFromEnv(
421
+ firstDefined(overrides.ssl,cliOptions.ssl,serverProfile?.ssl,process.env.DB_SSL),
422
+ engine!=="sqlserver"
423
+ )
424
+ };
425
+
426
+ const requiredForAuth=integratedAuth? ["host","database"]:["host","database","user","password"];
427
+ const missingWithoutConnString=requiredForAuth.filter((key) => {
428
+ return !config.connectionString&&!config[key];
429
+ });
430
+ if(missingWithoutConnString.length>0) {
431
+ throw new Error(`Missing DB config fields: ${missingWithoutConnString.join(", ")}`);
432
+ }
433
+
434
+ return config;
435
+ }
436
+
437
+ function createSqlServerConfig(config) {
438
+ const sharedPool={
439
+ max: intFromEnv(process.env.DB_POOL_MAX,10),
440
+ min: intFromEnv(process.env.DB_POOL_MIN,0),
441
+ idleTimeoutMillis: intFromEnv(process.env.DB_POOL_IDLE_TIMEOUT_MS,30000)
442
+ };
443
+
444
+ const sharedTimeouts={
445
+ connectionTimeout: intFromEnv(process.env.DB_CONN_TIMEOUT_MS,15000),
446
+ requestTimeout: intFromEnv(process.env.DB_REQUEST_TIMEOUT_MS,15000)
447
+ };
448
+
449
+ if(config.connectionString) {
450
+ const parsed=sql.ConnectionPool.parseConnectionString(config.connectionString);
451
+ return {
452
+ ...parsed,
453
+ ...sharedTimeouts,
454
+ options: {
455
+ ...(parsed.options??{}),
456
+ encrypt: config.encrypt,
457
+ trustServerCertificate: config.trustServerCertificate
458
+ },
459
+ pool: {
460
+ ...(parsed.pool??{}),
461
+ ...sharedPool
462
+ }
463
+ };
464
+ }
465
+
466
+ const baseConfig={
467
+ server: config.host,
468
+ port: config.port,
469
+ database: config.database,
470
+ options: {
471
+ encrypt: config.encrypt,
472
+ trustServerCertificate: config.trustServerCertificate
473
+ },
474
+ pool: sharedPool,
475
+ ...sharedTimeouts
476
+ };
477
+
478
+ if(config.integratedAuth) {
479
+ baseConfig.authentication={
480
+ type: "default",
481
+ options: {
482
+ userName: undefined,
483
+ password: undefined
484
+ }
485
+ };
486
+ } else {
487
+ baseConfig.user=config.user;
488
+ baseConfig.password=config.password;
489
+ }
490
+
491
+ return baseConfig;
492
+ }
493
+
494
+ function createPostgresConfig(config) {
495
+ if(config.connectionString) {
496
+ return {
497
+ connectionString: config.connectionString,
498
+ ssl: config.ssl? {rejectUnauthorized: !config.trustServerCertificate}:false
499
+ };
500
+ }
501
+
502
+ return {
503
+ host: config.host,
504
+ port: config.port,
505
+ database: config.database,
506
+ user: config.user,
507
+ password: config.password,
508
+ ssl: config.ssl? {rejectUnauthorized: !config.trustServerCertificate}:false
509
+ };
510
+ }
511
+
512
+ function createMysqlConfig(config) {
513
+ if(config.connectionString) {
514
+ return {
515
+ uri: config.connectionString,
516
+ waitForConnections: true,
517
+ connectionLimit: intFromEnv(process.env.DB_POOL_MAX,10),
518
+ ssl: config.ssl
519
+ ? {rejectUnauthorized: !config.trustServerCertificate}
520
+ : undefined
521
+ };
522
+ }
523
+
524
+ return {
525
+ host: config.host,
526
+ port: config.port,
527
+ database: config.database,
528
+ user: config.user,
529
+ password: config.password,
530
+ waitForConnections: true,
531
+ connectionLimit: intFromEnv(process.env.DB_POOL_MAX,10),
532
+ ssl: config.ssl
533
+ ? {rejectUnauthorized: !config.trustServerCertificate}
534
+ : undefined
535
+ };
536
+ }
537
+
538
+ async function closePoolIfAny() {
539
+ if(!connection?.client) {
540
+ return;
541
+ }
542
+
543
+ try {
544
+ if(connection.engine==="sqlserver") {
545
+ await connection.client.close();
546
+ } else if(connection.engine==="postgres") {
547
+ await connection.client.end();
548
+ } else if(connection.engine==="mysql") {
549
+ await connection.client.end();
550
+ }
551
+ } finally {
552
+ connection=null;
553
+ }
554
+ }
555
+
556
+ function compilePositionalQuery(sqlText,params,style) {
557
+ const values=[];
558
+ const pgIndexByName=new Map();
559
+
560
+ const text=sqlText.replace(/@([A-Za-z0-9_]+)/g,(_,name) => {
561
+ if(!(name in params)) {
562
+ throw new Error(`Missing parameter value: ${name}`);
563
+ }
564
+
565
+ if(style==="pg") {
566
+ if(!pgIndexByName.has(name)) {
567
+ pgIndexByName.set(name,values.length+1);
568
+ values.push(params[name]);
569
+ }
570
+ return `$${pgIndexByName.get(name)}`;
571
+ }
572
+
573
+ values.push(params[name]);
574
+ return "?";
575
+ });
576
+
577
+ return {text,values};
578
+ }
579
+
580
+ function normalizeQueryParams(params) {
581
+ if(!params||typeof params!=="object") {
582
+ return {};
583
+ }
584
+
585
+ for(const key of Object.keys(params)) {
586
+ if(!/^[A-Za-z0-9_]+$/.test(key)) {
587
+ throw new Error(`Invalid parameter name: ${key}`);
588
+ }
589
+ }
590
+
591
+ return params;
592
+ }
593
+
594
+ async function runSelectOne() {
595
+ if(!connection) {
596
+ throw new Error("Not connected.");
597
+ }
598
+
599
+ if(connection.engine==="sqlserver") {
600
+ const result=await connection.client.request().query("SELECT 1 AS ok");
601
+ return result.recordset?.[0]?.ok===1;
602
+ }
603
+
604
+ if(connection.engine==="postgres") {
605
+ const result=await connection.client.query("SELECT 1 AS ok");
606
+ return result.rows?.[0]?.ok===1;
607
+ }
608
+
609
+ const [rows]=await connection.client.query("SELECT 1 AS ok");
610
+ return rows?.[0]?.ok===1;
611
+ }
612
+
613
+ async function runQuery(sqlText,params={}) {
614
+ if(!connection) {
615
+ throw new Error("Not connected.");
616
+ }
617
+
618
+ const normalizedParams=normalizeQueryParams(params);
619
+
620
+ if(connection.engine==="sqlserver") {
621
+ const req=connection.client.request();
622
+ for(const [key,value] of Object.entries(normalizedParams)) {
623
+ req.input(key,value);
624
+ }
625
+ const result=await req.query(sqlText);
626
+ return result.recordset??[];
627
+ }
628
+
629
+ if(connection.engine==="postgres") {
630
+ const compiled=compilePositionalQuery(sqlText,normalizedParams,"pg");
631
+ const result=await connection.client.query(compiled.text,compiled.values);
632
+ return result.rows??[];
633
+ }
634
+
635
+ const compiled=compilePositionalQuery(sqlText,normalizedParams,"mysql");
636
+ const [rows]=await connection.client.query(compiled.text,compiled.values);
637
+ return Array.isArray(rows)? rows:[];
638
+ }
639
+
640
+ async function connectPool(overrides={}) {
641
+ const config=buildConfig(overrides);
642
+
643
+ await closePoolIfAny();
644
+
645
+ if(config.engine==="sqlserver") {
646
+ const sqlConfig=createSqlServerConfig(config);
647
+ const client=await new sql.ConnectionPool(sqlConfig).connect();
648
+ connection={engine: "sqlserver",client};
649
+ } else if(config.engine==="postgres") {
650
+ const pgConfig=createPostgresConfig(config);
651
+ const client=new PostgresPool(pgConfig);
652
+ await client.query("SELECT 1");
653
+ connection={engine: "postgres",client};
654
+ } else {
655
+ const mysqlConfig=createMysqlConfig(config);
656
+ const client=mysql.createPool(mysqlConfig);
657
+ await client.query("SELECT 1");
658
+ connection={engine: "mysql",client};
659
+ }
660
+
661
+ runtimeConfig={...config};
662
+ runtimeSettings={
663
+ readOnly: config.readOnly,
664
+ maxRows: config.maxRows
665
+ };
666
+
667
+ return {
668
+ connected: true,
669
+ alias: config.alias,
670
+ serverName: config.serverName,
671
+ profileSource: config.profileSource,
672
+ engine: config.engine,
673
+ host: config.host,
674
+ database: config.database,
675
+ readOnly: runtimeSettings.readOnly,
676
+ maxRows: runtimeSettings.maxRows
677
+ };
678
+ }
679
+
680
+ async function ensureConnected() {
681
+ if(connection?.client) {
682
+ return connection;
683
+ }
684
+
685
+ if(runtimeConfig) {
686
+ await connectPool(runtimeConfig);
687
+ return connection;
688
+ }
689
+
690
+ await connectPool();
691
+ return connection;
692
+ }
693
+
694
+ function makeTextResult(data) {
695
+ return {
696
+ content: [
697
+ {
698
+ type: "text",
699
+ text: JSON.stringify(data,null,2)
700
+ }
701
+ ]
702
+ };
703
+ }
704
+
705
+ function normalizeIdentifier(input,label) {
706
+ if(typeof input!=="string"||input.trim().length===0) {
707
+ throw new Error(`${label} is required.`);
708
+ }
709
+
710
+ const value=input.trim();
711
+ if(!/^[A-Za-z0-9_]+$/.test(value)) {
712
+ throw new Error(`${label} contains invalid characters.`);
713
+ }
714
+
715
+ return value;
716
+ }
717
+
718
+ function getReadOnlyMode() {
719
+ return runtimeSettings.readOnly;
720
+ }
721
+
722
+ function getDefaultMaxRows() {
723
+ return runtimeSettings.maxRows;
724
+ }
725
+
726
+ function validateQuerySafety(sqlText) {
727
+ const trimmed=String(sqlText??"").trim();
728
+ if(!trimmed) {
729
+ throw new Error("sql is required.");
730
+ }
731
+
732
+ if(trimmed.includes(";")) {
733
+ throw new Error("Semicolons are not allowed.");
734
+ }
735
+
736
+ if(!/^select\b/i.test(trimmed)) {
737
+ throw new Error("Only SELECT statements are allowed.");
738
+ }
739
+
740
+ const blockedPatterns=[
741
+ /\b(insert|update|delete|drop|alter|create|truncate|merge|exec|execute|grant|revoke)\b/i,
742
+ /--/,
743
+ /\/\*/
744
+ ];
745
+
746
+ for(const pattern of blockedPatterns) {
747
+ if(pattern.test(trimmed)) {
748
+ throw new Error("Query contains blocked syntax.");
749
+ }
750
+ }
751
+
752
+ return trimmed;
753
+ }
754
+
755
+ async function handleToolCall(name,args={}) {
756
+ switch(name) {
757
+ case TOOL_NAMES.CONNECT: {
758
+ const overrides={
759
+ ...(args.alias? {alias: args.alias}:{}),
760
+ ...(args.engine? {engine: args.engine}:{}),
761
+ ...(args.connectionString? {connectionString: args.connectionString}:{}),
762
+ ...(args.host? {host: args.host}:{}),
763
+ ...(args.server? {server: args.server}:{}),
764
+ ...(args.port? {port: Number(args.port)}:{}),
765
+ ...(args.database? {database: args.database}:{}),
766
+ ...(args.user? {user: args.user}:{}),
767
+ ...(args.password? {password: args.password}:{}),
768
+ ...(args.readOnly!==undefined? {readOnly: Boolean(args.readOnly)}:{}),
769
+ ...(args.maxRows!==undefined? {maxRows: Number(args.maxRows)}:{}),
770
+ ...(args.encrypt!==undefined? {encrypt: Boolean(args.encrypt)}:{}),
771
+ ...(args.ssl!==undefined? {ssl: Boolean(args.ssl)}:{}),
772
+ ...(args.trustServerCertificate!==undefined
773
+ ? {trustServerCertificate: Boolean(args.trustServerCertificate)}
774
+ :{})
775
+ };
776
+
777
+ const result=await connectPool(overrides);
778
+ const versionRows=connection.engine==="sqlserver"
779
+ ? await runQuery("SELECT @@VERSION AS version")
780
+ : await runQuery("SELECT VERSION() AS version").catch(() => [{version: "unknown"}]);
781
+
782
+ return makeTextResult({
783
+ ...result,
784
+ version: versionRows?.[0]?.version??"unknown"
785
+ });
786
+ }
787
+
788
+ case TOOL_NAMES.HEALTH_CHECK: {
789
+ await ensureConnected();
790
+ const started=Date.now();
791
+ const ok=await runSelectOne();
792
+ const latencyMs=Date.now()-started;
793
+ return makeTextResult({
794
+ ok,
795
+ engine: connection?.engine,
796
+ latencyMs
797
+ });
798
+ }
799
+
800
+ case TOOL_NAMES.LIST_SCHEMAS: {
801
+ await ensureConnected();
802
+ const schemas=await runQuery(`
803
+ SELECT schema_name
804
+ FROM information_schema.schemata
805
+ ORDER BY schema_name
806
+ `);
807
+ return makeTextResult({engine: connection.engine,schemas});
808
+ }
809
+
810
+ case TOOL_NAMES.LIST_TABLES: {
811
+ await ensureConnected();
812
+
813
+ let query=`
814
+ SELECT table_schema, table_name, table_type
815
+ FROM information_schema.tables
816
+ `;
817
+ const params={};
818
+
819
+ if(args.schema) {
820
+ params.schema=normalizeIdentifier(args.schema,"schema");
821
+ query+=" WHERE table_schema = @schema";
822
+ }
823
+
824
+ query+=" ORDER BY table_schema, table_name";
825
+
826
+ const tables=await runQuery(query,params);
827
+ return makeTextResult({engine: connection.engine,tables});
828
+ }
829
+
830
+ case TOOL_NAMES.DESCRIBE_TABLE: {
831
+ await ensureConnected();
832
+
833
+ const schema=normalizeIdentifier(args.schema??"dbo","schema");
834
+ const table=normalizeIdentifier(args.table,"table");
835
+
836
+ const columns=await runQuery(`
837
+ SELECT
838
+ c.column_name,
839
+ c.data_type,
840
+ c.character_maximum_length,
841
+ c.numeric_precision,
842
+ c.numeric_scale,
843
+ c.is_nullable,
844
+ c.column_default,
845
+ c.ordinal_position
846
+ FROM information_schema.columns c
847
+ WHERE c.table_schema = @schema AND c.table_name = @table
848
+ ORDER BY c.ordinal_position
849
+ `,{schema,table});
850
+
851
+ const primaryKeys=await runQuery(`
852
+ SELECT kcu.column_name
853
+ FROM information_schema.table_constraints tc
854
+ JOIN information_schema.key_column_usage kcu
855
+ ON tc.constraint_name = kcu.constraint_name
856
+ AND tc.table_schema = kcu.table_schema
857
+ WHERE tc.constraint_type = 'PRIMARY KEY'
858
+ AND tc.table_schema = @schema
859
+ AND tc.table_name = @table
860
+ ORDER BY kcu.ordinal_position
861
+ `,{schema,table});
862
+
863
+ let indexes=[];
864
+ if(connection.engine==="sqlserver") {
865
+ indexes=await runQuery(`
866
+ SELECT
867
+ i.name AS index_name,
868
+ i.is_unique,
869
+ i.is_primary_key,
870
+ c.name AS column_name
871
+ FROM sys.indexes i
872
+ JOIN sys.index_columns ic
873
+ ON i.object_id = ic.object_id AND i.index_id = ic.index_id
874
+ JOIN sys.columns c
875
+ ON c.object_id = ic.object_id AND c.column_id = ic.column_id
876
+ JOIN sys.tables t
877
+ ON t.object_id = i.object_id
878
+ JOIN sys.schemas s
879
+ ON s.schema_id = t.schema_id
880
+ WHERE s.name = @schema
881
+ AND t.name = @table
882
+ AND i.is_hypothetical = 0
883
+ ORDER BY i.name, ic.key_ordinal
884
+ `,{schema,table});
885
+ } else if(connection.engine==="postgres") {
886
+ indexes=await runQuery(`
887
+ SELECT indexname AS index_name, indexdef
888
+ FROM pg_indexes
889
+ WHERE schemaname = @schema
890
+ AND tablename = @table
891
+ ORDER BY indexname
892
+ `,{schema,table});
893
+ } else {
894
+ indexes=await runQuery(`
895
+ SELECT
896
+ index_name,
897
+ non_unique,
898
+ column_name,
899
+ seq_in_index
900
+ FROM information_schema.statistics
901
+ WHERE table_schema = @schema
902
+ AND table_name = @table
903
+ ORDER BY index_name, seq_in_index
904
+ `,{schema,table});
905
+ }
906
+
907
+ return makeTextResult({
908
+ engine: connection.engine,
909
+ schema,
910
+ table,
911
+ columns,
912
+ primaryKeys,
913
+ indexes
914
+ });
915
+ }
916
+
917
+ case TOOL_NAMES.QUERY: {
918
+ await ensureConnected();
919
+
920
+ const sqlText=validateQuerySafety(args.sql);
921
+ if(getReadOnlyMode()) {
922
+ validateQuerySafety(sqlText);
923
+ }
924
+
925
+ const maxRowsInput=args.maxRows!==undefined? Number(args.maxRows):getDefaultMaxRows();
926
+ const maxRows=Number.isFinite(maxRowsInput)&&maxRowsInput>0? Math.min(maxRowsInput,2000):200;
927
+
928
+ const started=Date.now();
929
+ const rows=await runQuery(sqlText,args.params&&typeof args.params==="object"? args.params:{});
930
+ const latencyMs=Date.now()-started;
931
+ const limitedRows=rows.slice(0,maxRows);
932
+
933
+ return makeTextResult({
934
+ engine: connection.engine,
935
+ rowCount: rows.length,
936
+ returnedRows: limitedRows.length,
937
+ maxRows,
938
+ latencyMs,
939
+ rows: limitedRows
940
+ });
941
+ }
942
+
943
+ default:
944
+ throw new Error(`Unknown tool: ${name}`);
945
+ }
946
+ }
947
+
948
+ const server=new Server(
949
+ {
950
+ name: "database-mcp",
951
+ version: SERVER_VERSION
952
+ },
953
+ {
954
+ capabilities: {
955
+ tools: {}
956
+ }
957
+ }
958
+ );
959
+
960
+ server.setRequestHandler(ListToolsRequestSchema,async () => {
961
+ return {
962
+ tools: [
963
+ {
964
+ name: TOOL_NAMES.CONNECT,
965
+ description: "Connect to a database (sqlserver, postgres, mysql) with optional alias/runtime overrides.",
966
+ annotations: {
967
+ readOnlyHint: true
968
+ },
969
+ inputSchema: {
970
+ type: "object",
971
+ properties: {
972
+ alias: {type: "string"},
973
+ serverName: {type: "string"},
974
+ engine: {type: "string",enum: ["sqlserver","postgres","mysql"]},
975
+ connectionString: {type: "string"},
976
+ host: {type: "string"},
977
+ server: {type: "string"},
978
+ port: {type: "number"},
979
+ database: {type: "string"},
980
+ user: {type: "string"},
981
+ password: {type: "string"},
982
+ encrypt: {type: "boolean"},
983
+ ssl: {type: "boolean"},
984
+ trustServerCertificate: {type: "boolean"},
985
+ readOnly: {type: "boolean"},
986
+ maxRows: {type: "number"}
987
+ }
988
+ }
989
+ },
990
+ {
991
+ name: TOOL_NAMES.HEALTH_CHECK,
992
+ description: "Check current database connection health.",
993
+ annotations: {
994
+ readOnlyHint: true
995
+ },
996
+ inputSchema: {
997
+ type: "object",
998
+ properties: {}
999
+ }
1000
+ },
1001
+ {
1002
+ name: TOOL_NAMES.LIST_SCHEMAS,
1003
+ description: "List available schemas.",
1004
+ annotations: {
1005
+ readOnlyHint: true
1006
+ },
1007
+ inputSchema: {
1008
+ type: "object",
1009
+ properties: {}
1010
+ }
1011
+ },
1012
+ {
1013
+ name: TOOL_NAMES.LIST_TABLES,
1014
+ description: "List tables and views, optionally filtered by schema.",
1015
+ annotations: {
1016
+ readOnlyHint: true
1017
+ },
1018
+ inputSchema: {
1019
+ type: "object",
1020
+ properties: {
1021
+ schema: {type: "string"}
1022
+ }
1023
+ }
1024
+ },
1025
+ {
1026
+ name: TOOL_NAMES.DESCRIBE_TABLE,
1027
+ description: "Describe table columns, PKs and indexes.",
1028
+ annotations: {
1029
+ readOnlyHint: true
1030
+ },
1031
+ inputSchema: {
1032
+ type: "object",
1033
+ properties: {
1034
+ schema: {type: "string"},
1035
+ table: {type: "string"}
1036
+ },
1037
+ required: ["table"]
1038
+ }
1039
+ },
1040
+ {
1041
+ name: TOOL_NAMES.QUERY,
1042
+ description: "Execute parameterized SELECT query.",
1043
+ annotations: {
1044
+ readOnlyHint: true
1045
+ },
1046
+ inputSchema: {
1047
+ type: "object",
1048
+ properties: {
1049
+ sql: {type: "string"},
1050
+ params: {
1051
+ type: "object",
1052
+ additionalProperties: true
1053
+ },
1054
+ maxRows: {type: "number"}
1055
+ },
1056
+ required: ["sql"]
1057
+ }
1058
+ }
1059
+ ]
1060
+ };
1061
+ });
1062
+
1063
+ server.setRequestHandler(CallToolRequestSchema,async (request) => {
1064
+ try {
1065
+ return await handleToolCall(request.params.name,request.params.arguments??{});
1066
+ } catch(error) {
1067
+ return {
1068
+ isError: true,
1069
+ content: [
1070
+ {
1071
+ type: "text",
1072
+ text: error instanceof Error? error.message:String(error)
1073
+ }
1074
+ ]
1075
+ };
1076
+ }
1077
+ });
1078
+
1079
+ async function main() {
1080
+ const transport=new StdioServerTransport();
1081
+ await server.connect(transport);
1082
+ }
1083
+
1084
+ main().catch(async (error) => {
1085
+ console.error("Fatal MCP server error:",error);
1086
+ await closePoolIfAny();
1087
+ process.exit(1);
1088
+ });
1089
+
1090
+ process.on("SIGINT",async () => {
1091
+ await closePoolIfAny();
1092
+ process.exit(0);
1093
+ });
1094
+
1095
+ process.on("SIGTERM",async () => {
1096
+ await closePoolIfAny();
1097
+ process.exit(0);
1098
+ });
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@kingsnow129/database-mcp",
3
+ "version": "0.4.0",
4
+ "mcpName": "io.github.kingsnow129/database-mcp",
5
+ "description": "Database MCP server for SQL Server, PostgreSQL, and MySQL with profile-based auto resolution",
6
+ "author": "kingsnow129",
7
+ "type": "module",
8
+ "main": "dist/server.js",
9
+ "bin": {
10
+ "database-mcp": "dist/server.js",
11
+ "sqlserver-mcp": "dist/server.js"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/kingsnow129/database-mcp.git"
16
+ },
17
+ "bugs": {
18
+ "url": "https://github.com/kingsnow129/database-mcp/issues"
19
+ },
20
+ "homepage": "https://github.com/kingsnow129/database-mcp#readme",
21
+ "engines": {
22
+ "node": ">=18"
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "README.md",
27
+ ".env.example",
28
+ "LICENSE"
29
+ ],
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "scripts": {
34
+ "build": "node scripts/build.mjs",
35
+ "prepare": "npm run build",
36
+ "start": "node dist/server.js",
37
+ "dev": "node src/server.js",
38
+ "pack:check": "npm run build && npm pack --dry-run"
39
+ },
40
+ "keywords": [
41
+ "mcp",
42
+ "postgres",
43
+ "mysql",
44
+ "sqlserver",
45
+ "database"
46
+ ],
47
+ "license": "MIT",
48
+ "dependencies": {
49
+ "@modelcontextprotocol/sdk": "^1.12.0",
50
+ "dotenv": "^16.4.5",
51
+ "mysql2": "^3.11.3",
52
+ "mssql": "^11.0.1",
53
+ "pg": "^8.13.1"
54
+ }
55
+ }