@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 +23 -0
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/dist/server.js +1098 -0
- package/package.json +55 -0
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
|
+
}
|