@eamonboyle/mssql-mcp 1.0.0 → 1.2.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/README.md +153 -36
- package/dist/__tests__/ExplainQueryTool.test.d.ts +2 -0
- package/dist/__tests__/ExplainQueryTool.test.d.ts.map +1 -0
- package/dist/__tests__/ExplainQueryTool.test.js +96 -0
- package/dist/__tests__/ExplainQueryTool.test.js.map +1 -0
- package/dist/__tests__/config.test.d.ts +2 -0
- package/dist/__tests__/config.test.d.ts.map +1 -0
- package/dist/__tests__/config.test.js +29 -0
- package/dist/__tests__/config.test.js.map +1 -0
- package/dist/__tests__/db.test.d.ts +2 -0
- package/dist/__tests__/db.test.d.ts.map +1 -0
- package/dist/__tests__/db.test.js +204 -0
- package/dist/__tests__/db.test.js.map +1 -0
- package/dist/__tests__/promptRegistry.test.d.ts +2 -0
- package/dist/__tests__/promptRegistry.test.d.ts.map +1 -0
- package/dist/__tests__/promptRegistry.test.js +40 -0
- package/dist/__tests__/promptRegistry.test.js.map +1 -0
- package/dist/__tests__/resourceRegistry.test.d.ts +2 -0
- package/dist/__tests__/resourceRegistry.test.d.ts.map +1 -0
- package/dist/__tests__/resourceRegistry.test.js +40 -0
- package/dist/__tests__/resourceRegistry.test.js.map +1 -0
- package/dist/__tests__/schema.test.d.ts +2 -0
- package/dist/__tests__/schema.test.d.ts.map +1 -0
- package/dist/__tests__/schema.test.js +130 -0
- package/dist/__tests__/schema.test.js.map +1 -0
- package/dist/__tests__/sql.test.d.ts +2 -0
- package/dist/__tests__/sql.test.d.ts.map +1 -0
- package/dist/__tests__/sql.test.js +35 -0
- package/dist/__tests__/sql.test.js.map +1 -0
- package/dist/__tests__/toolRegistry.test.d.ts +2 -0
- package/dist/__tests__/toolRegistry.test.d.ts.map +1 -0
- package/dist/__tests__/toolRegistry.test.js +47 -0
- package/dist/__tests__/toolRegistry.test.js.map +1 -0
- package/dist/__tests__/validation.test.d.ts +2 -0
- package/dist/__tests__/validation.test.d.ts.map +1 -0
- package/dist/__tests__/validation.test.js +111 -0
- package/dist/__tests__/validation.test.js.map +1 -0
- package/dist/__tests__/writeTools.test.d.ts +2 -0
- package/dist/__tests__/writeTools.test.d.ts.map +1 -0
- package/dist/__tests__/writeTools.test.js +115 -0
- package/dist/__tests__/writeTools.test.js.map +1 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +34 -0
- package/dist/config.js.map +1 -0
- package/dist/db.d.ts +28 -24
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +144 -89
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +70 -105
- package/dist/index.js.map +1 -1
- package/dist/promptRegistry.d.ts +16 -0
- package/dist/promptRegistry.d.ts.map +1 -0
- package/dist/promptRegistry.js +107 -0
- package/dist/promptRegistry.js.map +1 -0
- package/dist/resourceRegistry.d.ts +12 -0
- package/dist/resourceRegistry.d.ts.map +1 -0
- package/dist/resourceRegistry.js +172 -0
- package/dist/resourceRegistry.js.map +1 -0
- package/dist/schema.d.ts +41 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +211 -0
- package/dist/schema.js.map +1 -0
- package/dist/sql.d.ts +7 -0
- package/dist/sql.d.ts.map +1 -0
- package/dist/sql.js +73 -0
- package/dist/sql.js.map +1 -0
- package/dist/toolRegistry.d.ts +16 -0
- package/dist/toolRegistry.d.ts.map +1 -0
- package/dist/toolRegistry.js +401 -0
- package/dist/toolRegistry.js.map +1 -0
- package/dist/tools/CreateIndexTool.d.ts +30 -23
- package/dist/tools/CreateIndexTool.d.ts.map +1 -1
- package/dist/tools/CreateIndexTool.js +47 -71
- package/dist/tools/CreateIndexTool.js.map +1 -1
- package/dist/tools/CreateTableTool.d.ts +18 -11
- package/dist/tools/CreateTableTool.d.ts.map +1 -1
- package/dist/tools/CreateTableTool.js +37 -56
- package/dist/tools/CreateTableTool.js.map +1 -1
- package/dist/tools/DeleteDataTool.d.ts +22 -0
- package/dist/tools/DeleteDataTool.d.ts.map +1 -0
- package/dist/tools/DeleteDataTool.js +35 -0
- package/dist/tools/DeleteDataTool.js.map +1 -0
- package/dist/tools/DescribeObjectTool.d.ts +21 -0
- package/dist/tools/DescribeObjectTool.d.ts.map +1 -0
- package/dist/tools/DescribeObjectTool.js +31 -0
- package/dist/tools/DescribeObjectTool.js.map +1 -0
- package/dist/tools/DescribeTableTool.d.ts +19 -20
- package/dist/tools/DescribeTableTool.d.ts.map +1 -1
- package/dist/tools/DescribeTableTool.js +23 -41
- package/dist/tools/DescribeTableTool.js.map +1 -1
- package/dist/tools/DropTableTool.d.ts +13 -11
- package/dist/tools/DropTableTool.d.ts.map +1 -1
- package/dist/tools/DropTableTool.js +30 -44
- package/dist/tools/DropTableTool.js.map +1 -1
- package/dist/tools/ExplainQueryTool.d.ts +32 -0
- package/dist/tools/ExplainQueryTool.d.ts.map +1 -0
- package/dist/tools/ExplainQueryTool.js +102 -0
- package/dist/tools/ExplainQueryTool.js.map +1 -0
- package/dist/tools/InsertDataTool.d.ts +20 -16
- package/dist/tools/InsertDataTool.d.ts.map +1 -1
- package/dist/tools/InsertDataTool.js +91 -108
- package/dist/tools/InsertDataTool.js.map +1 -1
- package/dist/tools/ListObjectsTool.d.ts +20 -0
- package/dist/tools/ListObjectsTool.d.ts.map +1 -0
- package/dist/tools/ListObjectsTool.js +26 -0
- package/dist/tools/ListObjectsTool.js.map +1 -0
- package/dist/tools/ListTableTool.d.ts +20 -16
- package/dist/tools/ListTableTool.d.ts.map +1 -1
- package/dist/tools/ListTableTool.js +32 -49
- package/dist/tools/ListTableTool.js.map +1 -1
- package/dist/tools/ReadDataTool.d.ts +35 -41
- package/dist/tools/ReadDataTool.d.ts.map +1 -1
- package/dist/tools/ReadDataTool.js +92 -228
- package/dist/tools/ReadDataTool.js.map +1 -1
- package/dist/tools/SearchDataTool.d.ts +27 -0
- package/dist/tools/SearchDataTool.d.ts.map +1 -0
- package/dist/tools/SearchDataTool.js +61 -0
- package/dist/tools/SearchDataTool.js.map +1 -0
- package/dist/tools/UpdateDataTool.d.ts +21 -16
- package/dist/tools/UpdateDataTool.d.ts.map +1 -1
- package/dist/tools/UpdateDataTool.js +45 -65
- package/dist/tools/UpdateDataTool.js.map +1 -1
- package/dist/validation.d.ts +14 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +134 -0
- package/dist/validation.js.map +1 -0
- package/dist/writeSafety.d.ts +21 -0
- package/dist/writeSafety.d.ts.map +1 -0
- package/dist/writeSafety.js +169 -0
- package/dist/writeSafety.js.map +1 -0
- package/package.json +17 -6
package/README.md
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# MSSQL MCP Server
|
|
2
2
|
|
|
3
3
|
[](https://opensource.org/licenses/MIT)
|
|
4
|
+
[](https://www.npmjs.com/package/@eamonboyle/mssql-mcp)
|
|
4
5
|
[](https://nodejs.org/)
|
|
5
6
|
|
|
6
|
-
[](cursor
|
|
7
|
-
[](
|
|
7
|
+
[](https://cursor.com/en/install-mcp?name=MSSQL&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBlYW1vbmJveWxlL21zc3FsLW1jcCJdLCJlbnYiOnsiU0VSVkVSX05BTUUiOiJsb2NhbGhvc3QiLCJEQVRBQkFTRV9OQU1FIjoiWW91ckRhdGFiYXNlIiwiREFUQUJBU0VTIjoiIiwiREJfVVNFUiI6IiIsIkRCX1BBU1NXT1JEIjoiIiwiUkVBRE9OTFkiOiJmYWxzZSIsIkNPTk5FQ1RJT05fVElNRU9VVCI6IjMwIiwiUVVFUllfVElNRU9VVF9NUyI6IjMwMDAwIiwiTUFYX1JPV1MiOiIxMDAwMCIsIlRSVVNUX1NFUlZFUl9DRVJUSUZJQ0FURSI6ImZhbHNlIn19)
|
|
8
|
+
[](https://intradeus.github.io/http-protocol-redirector?r=vscode%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522mssql%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522-y%2522%252C%2522%2540eamonboyle%252Fmssql-mcp%2522%255D%252C%2522env%2522%253A%257B%2522SERVER_NAME%2522%253A%2522localhost%2522%252C%2522DATABASE_NAME%2522%253A%2522YourDatabase%2522%252C%2522DATABASES%2522%253A%2522%2522%252C%2522DB_USER%2522%253A%2522%2522%252C%2522DB_PASSWORD%2522%253A%2522%2522%252C%2522READONLY%2522%253A%2522false%2522%252C%2522CONNECTION_TIMEOUT%2522%253A%252230%2522%252C%2522QUERY_TIMEOUT_MS%2522%253A%252230000%2522%252C%2522MAX_ROWS%2522%253A%252210000%2522%252C%2522TRUST_SERVER_CERTIFICATE%2522%253A%2522false%2522%257D%257D)
|
|
8
9
|
|
|
9
10
|
> ⚠️ **EXPERIMENTAL USE ONLY** — This MCP Server is provided for educational and experimental purposes. It is NOT intended for production use. Use appropriate security measures and test thoroughly before any deployment.
|
|
10
11
|
|
|
@@ -22,11 +23,13 @@ AI: *queries your MSSQL database and returns the results in plain English*
|
|
|
22
23
|
## Features 📊
|
|
23
24
|
|
|
24
25
|
- **Natural language to SQL** — Ask questions in plain English
|
|
25
|
-
- **CRUD
|
|
26
|
-
- **Schema
|
|
26
|
+
- **Row-level CRUD support** — Read, insert, update, and delete rows with dedicated tools
|
|
27
|
+
- **Schema discovery** — Inspect tables, views, procedures, functions, and triggers
|
|
28
|
+
- **Query analysis** — Generate estimated execution plans with `explain_query`
|
|
29
|
+
- **MCP resources and prompts** — Expose schema snapshots and prompt templates to capable clients
|
|
27
30
|
- **Multi-database support** — Connect to multiple databases on the same server
|
|
28
|
-
- **Read-only mode** — Restrict to
|
|
29
|
-
- **Secure by default** — WHERE clauses required for
|
|
31
|
+
- **Read-only mode** — Restrict to inspection, search, read, and explain tools for safer environments
|
|
32
|
+
- **Secure by default** — WHERE clauses required for updates/deletes; SQL injection safeguards for reads
|
|
30
33
|
|
|
31
34
|
## Supported AI Clients
|
|
32
35
|
|
|
@@ -39,7 +42,11 @@ AI: *queries your MSSQL database and returns the results in plain English*
|
|
|
39
42
|
|
|
40
43
|
### One-Click Install (Cursor / VS Code)
|
|
41
44
|
|
|
42
|
-
Click **Add to Cursor** or **Install in VS Code** above to add the MCP server
|
|
45
|
+
Click **Add to Cursor** or **Install in VS Code** above to add the MCP server—no cloning required; it runs via `npx`.
|
|
46
|
+
|
|
47
|
+
**Cursor** opens a dedicated install page that lists env vars you can edit before saving (similar to a short form).
|
|
48
|
+
|
|
49
|
+
**VS Code** only applies the JSON embedded in the `vscode:mcp/install` link: you get static placeholder values, not an interactive database wizard. To be prompted for host, database, and credentials when the server starts, add [`inputs`](https://code.visualstudio.com/docs/copilot/reference/mcp-configuration#_input-variables-for-sensitive-data) to `.vscode/mcp.json` as in the **Prompted inputs** example under **VS Code (`mcp.json`)** below.
|
|
43
50
|
|
|
44
51
|
### Prerequisites
|
|
45
52
|
|
|
@@ -70,22 +77,26 @@ npm run build
|
|
|
70
77
|
|
|
71
78
|
### Environment Variables
|
|
72
79
|
|
|
73
|
-
| Variable
|
|
74
|
-
|
|
75
|
-
| `SERVER_NAME`
|
|
76
|
-
| `DATABASE_NAME`
|
|
77
|
-
| `DB_USER`
|
|
78
|
-
| `DB_PASSWORD`
|
|
79
|
-
| `READONLY`
|
|
80
|
-
| `DATABASES`
|
|
81
|
-
| `CONNECTION_TIMEOUT`
|
|
82
|
-
| `
|
|
80
|
+
| Variable | Required | Description |
|
|
81
|
+
| -------------------------- | -------- | ------------------------------------------------------------------------------------ |
|
|
82
|
+
| `SERVER_NAME` | Yes | SQL Server host (e.g., `localhost`, `my-server.database.windows.net`) |
|
|
83
|
+
| `DATABASE_NAME` | Yes\*\* | Default database name. Optional when `DATABASES` is set. |
|
|
84
|
+
| `DB_USER` | Yes\* | SQL Server username (for SQL authentication) |
|
|
85
|
+
| `DB_PASSWORD` | Yes\* | SQL Server password (for SQL authentication) |
|
|
86
|
+
| `READONLY` | No | `"true"` for read-only mode, `"false"` for full access (default: `"false"`) |
|
|
87
|
+
| `DATABASES` | No | Comma-separated allowlist for multi-database access (e.g., `ProdDB,StagingDB`) |
|
|
88
|
+
| `CONNECTION_TIMEOUT` | No | Timeout in seconds (default: `30`) |
|
|
89
|
+
| `QUERY_TIMEOUT_MS` | No | Query timeout in milliseconds (default: `30000`) |
|
|
90
|
+
| `MAX_ROWS` | No | Maximum rows returned by read tools (default: `10000`) |
|
|
91
|
+
| `TRUST_SERVER_CERTIFICATE` | No | `"true"` for self-signed certs (e.g., local dev) (default: `"false"`) |
|
|
83
92
|
|
|
84
93
|
\* Required for SQL authentication. For Windows/Integrated authentication, consult the [mssql](https://www.npmjs.com/package/mssql) package documentation.
|
|
85
94
|
|
|
86
|
-
|
|
95
|
+
\*\* Required for single-database setups. When `DATABASES` is provided, `DATABASE_NAME` becomes optional and is used as the default database if set.
|
|
96
|
+
|
|
97
|
+
### Cursor (`mcp.json`)
|
|
87
98
|
|
|
88
|
-
|
|
99
|
+
Use [global or project MCP config](https://cursor.com/docs/context/mcp): e.g. `~/.cursor/mcp.json` or `.cursor/mcp.json` in your repo.
|
|
89
100
|
|
|
90
101
|
```json
|
|
91
102
|
{
|
|
@@ -95,7 +106,8 @@ npm run build
|
|
|
95
106
|
"args": ["-y", "@eamonboyle/mssql-mcp"],
|
|
96
107
|
"env": {
|
|
97
108
|
"SERVER_NAME": "localhost",
|
|
98
|
-
"DATABASE_NAME": "
|
|
109
|
+
"DATABASE_NAME": "AppDB",
|
|
110
|
+
"DATABASES": "AppDB,ReportingDB",
|
|
99
111
|
"DB_USER": "your_username",
|
|
100
112
|
"DB_PASSWORD": "your_password",
|
|
101
113
|
"READONLY": "false"
|
|
@@ -105,9 +117,93 @@ npm run build
|
|
|
105
117
|
}
|
|
106
118
|
```
|
|
107
119
|
|
|
108
|
-
|
|
120
|
+
Restart Cursor after changes.
|
|
121
|
+
|
|
122
|
+
### VS Code (`mcp.json`)
|
|
123
|
+
|
|
124
|
+
VS Code uses `.vscode/mcp.json` (or **MCP: Open User Configuration**) with a top-level [`servers`](https://code.visualstudio.com/docs/copilot/reference/mcp-configuration) object—**not** `mcpServers`.
|
|
125
|
+
|
|
126
|
+
**Static env** (same idea as the one-click link; edit values in the file):
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{
|
|
130
|
+
"servers": {
|
|
131
|
+
"mssql": {
|
|
132
|
+
"type": "stdio",
|
|
133
|
+
"command": "npx",
|
|
134
|
+
"args": ["-y", "@eamonboyle/mssql-mcp"],
|
|
135
|
+
"env": {
|
|
136
|
+
"SERVER_NAME": "localhost",
|
|
137
|
+
"DATABASE_NAME": "AppDB",
|
|
138
|
+
"DATABASES": "AppDB,ReportingDB",
|
|
139
|
+
"DB_USER": "your_username",
|
|
140
|
+
"DB_PASSWORD": "your_password",
|
|
141
|
+
"READONLY": "false",
|
|
142
|
+
"CONNECTION_TIMEOUT": "30",
|
|
143
|
+
"QUERY_TIMEOUT_MS": "30000",
|
|
144
|
+
"MAX_ROWS": "10000",
|
|
145
|
+
"TRUST_SERVER_CERTIFICATE": "false"
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Prompted inputs** (closest to Cursor’s hosted form: VS Code asks on first start, then stores values). Use `${input:…}` in `env` and define matching entries under `inputs`:
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"inputs": [
|
|
157
|
+
{
|
|
158
|
+
"type": "promptString",
|
|
159
|
+
"id": "mssql-server",
|
|
160
|
+
"description": "SQL Server host (e.g. localhost or my-server.database.windows.net)"
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"type": "promptString",
|
|
164
|
+
"id": "mssql-database",
|
|
165
|
+
"description": "Default database name"
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
"type": "promptString",
|
|
169
|
+
"id": "mssql-databases",
|
|
170
|
+
"description": "Optional: comma-separated DB allowlist (e.g. AppDB,ReportingDB). Leave empty for a single database."
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"type": "promptString",
|
|
174
|
+
"id": "mssql-user",
|
|
175
|
+
"description": "SQL Server login (SQL authentication)"
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"type": "promptString",
|
|
179
|
+
"id": "mssql-password",
|
|
180
|
+
"description": "SQL Server password",
|
|
181
|
+
"password": true
|
|
182
|
+
}
|
|
183
|
+
],
|
|
184
|
+
"servers": {
|
|
185
|
+
"mssql": {
|
|
186
|
+
"type": "stdio",
|
|
187
|
+
"command": "npx",
|
|
188
|
+
"args": ["-y", "@eamonboyle/mssql-mcp"],
|
|
189
|
+
"env": {
|
|
190
|
+
"SERVER_NAME": "${input:mssql-server}",
|
|
191
|
+
"DATABASE_NAME": "${input:mssql-database}",
|
|
192
|
+
"DATABASES": "${input:mssql-databases}",
|
|
193
|
+
"DB_USER": "${input:mssql-user}",
|
|
194
|
+
"DB_PASSWORD": "${input:mssql-password}",
|
|
195
|
+
"READONLY": "false",
|
|
196
|
+
"CONNECTION_TIMEOUT": "30",
|
|
197
|
+
"QUERY_TIMEOUT_MS": "30000",
|
|
198
|
+
"MAX_ROWS": "10000",
|
|
199
|
+
"TRUST_SERVER_CERTIFICATE": "false"
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
```
|
|
109
205
|
|
|
110
|
-
###
|
|
206
|
+
### Claude Desktop Setup
|
|
111
207
|
|
|
112
208
|
1. Open **File → Settings → Developer → Edit Config**
|
|
113
209
|
2. Add the MCP server configuration:
|
|
@@ -120,7 +216,8 @@ npm run build
|
|
|
120
216
|
"args": ["-y", "@eamonboyle/mssql-mcp"],
|
|
121
217
|
"env": {
|
|
122
218
|
"SERVER_NAME": "localhost",
|
|
123
|
-
"DATABASE_NAME": "
|
|
219
|
+
"DATABASE_NAME": "AppDB",
|
|
220
|
+
"DATABASES": "AppDB,ReportingDB",
|
|
124
221
|
"DB_USER": "your_username",
|
|
125
222
|
"DB_PASSWORD": "your_password",
|
|
126
223
|
"READONLY": "false"
|
|
@@ -147,7 +244,7 @@ To allow queries across multiple databases:
|
|
|
147
244
|
}
|
|
148
245
|
```
|
|
149
246
|
|
|
150
|
-
All tools accept an optional `databaseName` parameter. When omitted, `DATABASE_NAME` is
|
|
247
|
+
`DATABASES` defines which databases the MCP can access. All tools accept an optional `databaseName` parameter. When omitted, the server uses `DATABASE_NAME` if it is included in `DATABASES`; otherwise it falls back to the first entry in `DATABASES`.
|
|
151
248
|
|
|
152
249
|
## Sample Configurations
|
|
153
250
|
|
|
@@ -161,34 +258,54 @@ See `src/samples/` for example configs:
|
|
|
161
258
|
Once configured, you can ask things like:
|
|
162
259
|
|
|
163
260
|
- "Show me all users from New York"
|
|
261
|
+
- "Search the customers table for email addresses containing acme.com"
|
|
164
262
|
- "Create a new table called products with columns for id, name, and price"
|
|
165
263
|
- "Update all pending orders to completed status"
|
|
264
|
+
- "Delete inactive sessions older than 30 days"
|
|
166
265
|
- "List all tables in the database"
|
|
167
266
|
- "Describe the schema of the customers table"
|
|
267
|
+
- "List all views and procedures in the reporting database"
|
|
268
|
+
- "Explain why this SELECT query is slow"
|
|
168
269
|
|
|
169
270
|
## Available Tools
|
|
170
271
|
|
|
171
|
-
| Tool
|
|
172
|
-
|
|
173
|
-
| `read_data`
|
|
174
|
-
| `
|
|
175
|
-
| `
|
|
176
|
-
| `
|
|
177
|
-
| `
|
|
178
|
-
| `
|
|
179
|
-
| `
|
|
180
|
-
| `
|
|
272
|
+
| Tool | Read-only | Description |
|
|
273
|
+
| ----------------- | --------- | ------------------------------------------------------- |
|
|
274
|
+
| `read_data` | ✓ | Execute validated SELECT queries |
|
|
275
|
+
| `search_data` | ✓ | Search one or more columns with parameterized `LIKE` |
|
|
276
|
+
| `explain_query` | ✓ | Get an estimated execution plan for a SELECT query |
|
|
277
|
+
| `list_table` | ✓ | List tables in a database |
|
|
278
|
+
| `describe_table` | ✓ | Get table schema (optional `schemaName`) |
|
|
279
|
+
| `list_objects` | ✓ | List tables, views, procedures, functions, and triggers |
|
|
280
|
+
| `describe_object` | ✓ | Describe an object definition and metadata |
|
|
281
|
+
| `insert_data` | | Insert rows |
|
|
282
|
+
| `update_data` | | Update rows (requires WHERE) |
|
|
283
|
+
| `delete_data` | | Delete rows (requires WHERE) |
|
|
284
|
+
| `create_table` | | Create tables |
|
|
285
|
+
| `create_index` | | Create indexes |
|
|
286
|
+
| `drop_table` | | Drop tables |
|
|
287
|
+
|
|
288
|
+
## Resources And Prompts
|
|
289
|
+
|
|
290
|
+
Clients that support MCP resources and prompts can use additional discovery surfaces:
|
|
291
|
+
|
|
292
|
+
- **Resources** — Server config, prompt catalog, per-database table lists, per-database object lists, and dynamic table/object resources
|
|
293
|
+
- **Prompts** — `explore_schema`, `draft_safe_select`, and `review_write_operation`
|
|
294
|
+
|
|
295
|
+
## Changelog
|
|
296
|
+
|
|
297
|
+
Release notes: [CHANGELOG.md](https://github.com/eamonboyle/mssql-mcp/blob/main/CHANGELOG.md).
|
|
181
298
|
|
|
182
299
|
## Security Notes
|
|
183
300
|
|
|
184
301
|
- **Credentials** — Never commit `DB_USER`/`DB_PASSWORD` or config files with secrets. Use environment variables or a secrets manager.
|
|
185
302
|
- **Read-only mode** — Set `READONLY: "true"` when you only need queries.
|
|
186
|
-
- **WHERE clauses** —
|
|
303
|
+
- **WHERE clauses** — Update and delete operations require explicit WHERE clauses to reduce accidental full-table changes.
|
|
187
304
|
- **SQL injection** — The server validates and restricts dangerous SQL patterns.
|
|
188
305
|
|
|
189
306
|
## Contributing
|
|
190
307
|
|
|
191
|
-
Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
308
|
+
Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. By participating, you agree to uphold our [Code of Conduct](CODE_OF_CONDUCT.md).
|
|
192
309
|
|
|
193
310
|
## License
|
|
194
311
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExplainQueryTool.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/ExplainQueryTool.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
const explainMockState = vi.hoisted(() => ({
|
|
3
|
+
closeCalls: 0,
|
|
4
|
+
statements: [],
|
|
5
|
+
failOnQuery: false,
|
|
6
|
+
}));
|
|
7
|
+
const mockRequestModule = vi.hoisted(() => {
|
|
8
|
+
class MockRequest {
|
|
9
|
+
async query(statement) {
|
|
10
|
+
return this.run(statement);
|
|
11
|
+
}
|
|
12
|
+
async batch(statement) {
|
|
13
|
+
return this.run(statement);
|
|
14
|
+
}
|
|
15
|
+
async run(statement) {
|
|
16
|
+
explainMockState.statements.push(statement);
|
|
17
|
+
if (statement === "SET SHOWPLAN_XML ON") {
|
|
18
|
+
return { recordsets: [] };
|
|
19
|
+
}
|
|
20
|
+
if (statement === "SET SHOWPLAN_XML OFF") {
|
|
21
|
+
return { recordsets: [] };
|
|
22
|
+
}
|
|
23
|
+
if (explainMockState.failOnQuery) {
|
|
24
|
+
throw new Error("query failed");
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
recordsets: [[{ planXml: "<ShowPlanXML />" }]],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function mockPool() {
|
|
32
|
+
return {
|
|
33
|
+
request() {
|
|
34
|
+
return new MockRequest();
|
|
35
|
+
},
|
|
36
|
+
async close() {
|
|
37
|
+
explainMockState.closeCalls += 1;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return { MockRequest, mockPool };
|
|
42
|
+
});
|
|
43
|
+
vi.mock("mssql", () => ({
|
|
44
|
+
default: {
|
|
45
|
+
Request: mockRequestModule.MockRequest,
|
|
46
|
+
},
|
|
47
|
+
}));
|
|
48
|
+
vi.mock("../db.js", () => ({
|
|
49
|
+
getDedicatedSqlPool: vi.fn(async () => ({
|
|
50
|
+
pool: mockRequestModule.mockPool(),
|
|
51
|
+
})),
|
|
52
|
+
}));
|
|
53
|
+
import { ExplainQueryTool } from "../tools/ExplainQueryTool.js";
|
|
54
|
+
describe("ExplainQueryTool", () => {
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
vi.spyOn(console, "error").mockImplementation(() => { });
|
|
57
|
+
explainMockState.closeCalls = 0;
|
|
58
|
+
explainMockState.statements.length = 0;
|
|
59
|
+
explainMockState.failOnQuery = false;
|
|
60
|
+
});
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
vi.restoreAllMocks();
|
|
63
|
+
});
|
|
64
|
+
it("uses a dedicated pool and always disables SHOWPLAN_XML", async () => {
|
|
65
|
+
const tool = new ExplainQueryTool();
|
|
66
|
+
const result = await tool.run({
|
|
67
|
+
query: "SELECT * FROM movies",
|
|
68
|
+
});
|
|
69
|
+
expect(result).toMatchObject({
|
|
70
|
+
success: true,
|
|
71
|
+
planXml: "<ShowPlanXML />",
|
|
72
|
+
recordsets: undefined,
|
|
73
|
+
});
|
|
74
|
+
expect(explainMockState.statements).toEqual([
|
|
75
|
+
"SET SHOWPLAN_XML ON",
|
|
76
|
+
"SELECT * FROM movies",
|
|
77
|
+
"SET SHOWPLAN_XML OFF",
|
|
78
|
+
]);
|
|
79
|
+
expect(explainMockState.closeCalls).toBe(1);
|
|
80
|
+
});
|
|
81
|
+
it("cleans up SHOWPLAN state after failures", async () => {
|
|
82
|
+
explainMockState.failOnQuery = true;
|
|
83
|
+
const tool = new ExplainQueryTool();
|
|
84
|
+
const result = await tool.run({
|
|
85
|
+
query: "SELECT * FROM movies",
|
|
86
|
+
});
|
|
87
|
+
expect(result).toMatchObject({ success: false });
|
|
88
|
+
expect(explainMockState.statements).toEqual([
|
|
89
|
+
"SET SHOWPLAN_XML ON",
|
|
90
|
+
"SELECT * FROM movies",
|
|
91
|
+
"SET SHOWPLAN_XML OFF",
|
|
92
|
+
]);
|
|
93
|
+
expect(explainMockState.closeCalls).toBe(1);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
//# sourceMappingURL=ExplainQueryTool.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExplainQueryTool.test.js","sourceRoot":"","sources":["../../src/__tests__/ExplainQueryTool.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/config.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { clampRowLimit, getDefaultSearchLimit, getMaxRows, getQueryTimeoutMs, } from "../config.js";
|
|
3
|
+
const originalEnv = process.env;
|
|
4
|
+
afterEach(() => {
|
|
5
|
+
process.env = { ...originalEnv };
|
|
6
|
+
});
|
|
7
|
+
describe("config helpers", () => {
|
|
8
|
+
it("uses defaults when env vars are absent", () => {
|
|
9
|
+
delete process.env.MAX_ROWS;
|
|
10
|
+
delete process.env.QUERY_TIMEOUT_MS;
|
|
11
|
+
expect(getMaxRows()).toBe(10000);
|
|
12
|
+
expect(getQueryTimeoutMs()).toBe(30000);
|
|
13
|
+
expect(getDefaultSearchLimit()).toBe(100);
|
|
14
|
+
});
|
|
15
|
+
it("reads env-based limits", () => {
|
|
16
|
+
process.env.MAX_ROWS = "250";
|
|
17
|
+
process.env.QUERY_TIMEOUT_MS = "45000";
|
|
18
|
+
expect(getMaxRows()).toBe(250);
|
|
19
|
+
expect(getQueryTimeoutMs()).toBe(45000);
|
|
20
|
+
expect(getDefaultSearchLimit()).toBe(100);
|
|
21
|
+
});
|
|
22
|
+
it("clamps row limits to the configured maximum", () => {
|
|
23
|
+
process.env.MAX_ROWS = "50";
|
|
24
|
+
expect(clampRowLimit(200)).toBe(50);
|
|
25
|
+
expect(clampRowLimit("5")).toBe(5);
|
|
26
|
+
expect(clampRowLimit(undefined)).toBe(50);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
//# sourceMappingURL=config.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.test.js","sourceRoot":"","sources":["../../src/__tests__/config.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AACzD,OAAO,EACL,aAAa,EACb,qBAAqB,EACrB,UAAU,EACV,iBAAiB,GAClB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC;AAEhC,SAAS,CAAC,GAAG,EAAE;IACb,OAAO,CAAC,GAAG,GAAG,EAAE,GAAG,WAAW,EAAE,CAAC;AACnC,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAChD,OAAO,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC;QAC5B,OAAO,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;QAEpC,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACjC,MAAM,CAAC,iBAAiB,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACxC,MAAM,CAAC,qBAAqB,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wBAAwB,EAAE,GAAG,EAAE;QAChC,OAAO,CAAC,GAAG,CAAC,QAAQ,GAAG,KAAK,CAAC;QAC7B,OAAO,CAAC,GAAG,CAAC,gBAAgB,GAAG,OAAO,CAAC;QAEvC,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC/B,MAAM,CAAC,iBAAiB,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACxC,MAAM,CAAC,qBAAqB,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,OAAO,CAAC,GAAG,CAAC,QAAQ,GAAG,IAAI,CAAC;QAE5B,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACpC,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACnC,MAAM,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/db.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
const mssqlMockState = vi.hoisted(() => ({
|
|
3
|
+
poolConfigs: [],
|
|
4
|
+
requestObject: { kind: "mock-request" },
|
|
5
|
+
connectCalls: vi.fn(),
|
|
6
|
+
closeCalls: vi.fn(),
|
|
7
|
+
requestCalls: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
// Mock mssql to avoid loading native deps and transitive issues in test env
|
|
10
|
+
vi.mock("mssql", () => {
|
|
11
|
+
class MockConnectionPool {
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this.config = config;
|
|
14
|
+
this.connected = false;
|
|
15
|
+
mssqlMockState.poolConfigs.push(config);
|
|
16
|
+
}
|
|
17
|
+
async connect() {
|
|
18
|
+
this.connected = true;
|
|
19
|
+
mssqlMockState.connectCalls(this.config);
|
|
20
|
+
return this;
|
|
21
|
+
}
|
|
22
|
+
async close() {
|
|
23
|
+
this.connected = false;
|
|
24
|
+
mssqlMockState.closeCalls(this.config);
|
|
25
|
+
}
|
|
26
|
+
request() {
|
|
27
|
+
mssqlMockState.requestCalls(this.config);
|
|
28
|
+
return mssqlMockState.requestObject;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
default: {
|
|
33
|
+
ConnectionPool: MockConnectionPool,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
import { getAllowedDatabases, getSqlRequest, resolveDatabaseName } from "../db.js";
|
|
38
|
+
describe("getAllowedDatabases", () => {
|
|
39
|
+
const originalEnv = process.env;
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
process.env = { ...originalEnv };
|
|
42
|
+
mssqlMockState.poolConfigs.length = 0;
|
|
43
|
+
mssqlMockState.connectCalls.mockClear();
|
|
44
|
+
mssqlMockState.closeCalls.mockClear();
|
|
45
|
+
mssqlMockState.requestCalls.mockClear();
|
|
46
|
+
});
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
process.env = originalEnv;
|
|
49
|
+
});
|
|
50
|
+
it("returns DATABASE_NAME when DATABASES is not set", () => {
|
|
51
|
+
delete process.env.DATABASES;
|
|
52
|
+
process.env.DATABASE_NAME = "MyDb";
|
|
53
|
+
expect(getAllowedDatabases()).toEqual(["MyDb"]);
|
|
54
|
+
});
|
|
55
|
+
it("returns empty array when neither DATABASES nor DATABASE_NAME is set", () => {
|
|
56
|
+
delete process.env.DATABASES;
|
|
57
|
+
delete process.env.DATABASE_NAME;
|
|
58
|
+
expect(getAllowedDatabases()).toEqual([]);
|
|
59
|
+
});
|
|
60
|
+
it("returns empty array when DATABASE_NAME is empty string", () => {
|
|
61
|
+
delete process.env.DATABASES;
|
|
62
|
+
process.env.DATABASE_NAME = "";
|
|
63
|
+
expect(getAllowedDatabases()).toEqual([]);
|
|
64
|
+
});
|
|
65
|
+
it("returns DATABASES split by comma when set", () => {
|
|
66
|
+
process.env.DATABASES = "ProdDB,StagingDB,AnalyticsDB";
|
|
67
|
+
expect(getAllowedDatabases()).toEqual([
|
|
68
|
+
"ProdDB",
|
|
69
|
+
"StagingDB",
|
|
70
|
+
"AnalyticsDB",
|
|
71
|
+
]);
|
|
72
|
+
});
|
|
73
|
+
it("trims whitespace from DATABASES entries", () => {
|
|
74
|
+
process.env.DATABASES = " ProdDB , StagingDB , AnalyticsDB ";
|
|
75
|
+
expect(getAllowedDatabases()).toEqual([
|
|
76
|
+
"ProdDB",
|
|
77
|
+
"StagingDB",
|
|
78
|
+
"AnalyticsDB",
|
|
79
|
+
]);
|
|
80
|
+
});
|
|
81
|
+
it("filters out empty entries from DATABASES", () => {
|
|
82
|
+
process.env.DATABASES = "ProdDB,,StagingDB,";
|
|
83
|
+
expect(getAllowedDatabases()).toEqual(["ProdDB", "StagingDB"]);
|
|
84
|
+
});
|
|
85
|
+
it("prefers DATABASES over DATABASE_NAME when both set", () => {
|
|
86
|
+
process.env.DATABASES = "ProdDB,StagingDB";
|
|
87
|
+
process.env.DATABASE_NAME = "OtherDb";
|
|
88
|
+
expect(getAllowedDatabases()).toEqual(["ProdDB", "StagingDB"]);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
describe("resolveDatabaseName", () => {
|
|
92
|
+
const originalEnv = process.env;
|
|
93
|
+
beforeEach(() => {
|
|
94
|
+
process.env = { ...originalEnv };
|
|
95
|
+
mssqlMockState.poolConfigs.length = 0;
|
|
96
|
+
mssqlMockState.connectCalls.mockClear();
|
|
97
|
+
mssqlMockState.closeCalls.mockClear();
|
|
98
|
+
mssqlMockState.requestCalls.mockClear();
|
|
99
|
+
});
|
|
100
|
+
afterEach(() => {
|
|
101
|
+
process.env = originalEnv;
|
|
102
|
+
});
|
|
103
|
+
it("returns param when valid and in allowed list", () => {
|
|
104
|
+
process.env.DATABASES = "ProdDB,StagingDB";
|
|
105
|
+
expect(resolveDatabaseName("ProdDB")).toBe("ProdDB");
|
|
106
|
+
expect(resolveDatabaseName("StagingDB")).toBe("StagingDB");
|
|
107
|
+
});
|
|
108
|
+
it("returns DATABASE_NAME when param omitted and DATABASES not set", () => {
|
|
109
|
+
process.env.DATABASE_NAME = "DefaultDb";
|
|
110
|
+
delete process.env.DATABASES;
|
|
111
|
+
expect(resolveDatabaseName()).toBe("DefaultDb");
|
|
112
|
+
});
|
|
113
|
+
it("returns DATABASE_NAME when it is in DATABASES", () => {
|
|
114
|
+
process.env.DATABASES = "ProdDB,StagingDB";
|
|
115
|
+
process.env.DATABASE_NAME = "StagingDB";
|
|
116
|
+
expect(resolveDatabaseName()).toBe("StagingDB");
|
|
117
|
+
});
|
|
118
|
+
it("returns the first DATABASES entry when DATABASE_NAME is omitted", () => {
|
|
119
|
+
process.env.DATABASES = "ProdDB,StagingDB";
|
|
120
|
+
delete process.env.DATABASE_NAME;
|
|
121
|
+
expect(resolveDatabaseName()).toBe("ProdDB");
|
|
122
|
+
});
|
|
123
|
+
it("returns param when param provided and in allowed list", () => {
|
|
124
|
+
process.env.DATABASES = "ProdDB,StagingDB";
|
|
125
|
+
process.env.DATABASE_NAME = "ProdDB";
|
|
126
|
+
expect(resolveDatabaseName("StagingDB")).toBe("StagingDB");
|
|
127
|
+
});
|
|
128
|
+
it("falls back to the first DATABASES entry when DATABASE_NAME is not included in DATABASES", () => {
|
|
129
|
+
process.env.DATABASES = "ProdDB,StagingDB";
|
|
130
|
+
process.env.DATABASE_NAME = "OtherDb";
|
|
131
|
+
expect(resolveDatabaseName()).toBe("ProdDB");
|
|
132
|
+
});
|
|
133
|
+
it("returns null when param not in allowed list", () => {
|
|
134
|
+
process.env.DATABASES = "ProdDB,StagingDB";
|
|
135
|
+
expect(resolveDatabaseName("OtherDb")).toBeNull();
|
|
136
|
+
});
|
|
137
|
+
it("returns null when param is empty and DATABASE_NAME not set", () => {
|
|
138
|
+
delete process.env.DATABASE_NAME;
|
|
139
|
+
delete process.env.DATABASES;
|
|
140
|
+
expect(resolveDatabaseName()).toBeNull();
|
|
141
|
+
expect(resolveDatabaseName("")).toBeNull();
|
|
142
|
+
});
|
|
143
|
+
it("returns resolved name when no DATABASES set (only DATABASE_NAME allowed)", () => {
|
|
144
|
+
delete process.env.DATABASES;
|
|
145
|
+
process.env.DATABASE_NAME = "MyDb";
|
|
146
|
+
expect(resolveDatabaseName()).toBe("MyDb");
|
|
147
|
+
expect(resolveDatabaseName("MyDb")).toBe("MyDb");
|
|
148
|
+
expect(resolveDatabaseName("AnyDb")).toBeNull();
|
|
149
|
+
});
|
|
150
|
+
it("trims whitespace from param", () => {
|
|
151
|
+
process.env.DATABASES = "ProdDB";
|
|
152
|
+
expect(resolveDatabaseName(" ProdDB ")).toBe("ProdDB");
|
|
153
|
+
});
|
|
154
|
+
it("returns null when neither DATABASE_NAME nor DATABASES is configured", () => {
|
|
155
|
+
delete process.env.DATABASE_NAME;
|
|
156
|
+
delete process.env.DATABASES;
|
|
157
|
+
expect(resolveDatabaseName()).toBeNull();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
describe("getSqlRequest", () => {
|
|
161
|
+
const originalEnv = process.env;
|
|
162
|
+
beforeEach(() => {
|
|
163
|
+
process.env = { ...originalEnv };
|
|
164
|
+
mssqlMockState.poolConfigs.length = 0;
|
|
165
|
+
mssqlMockState.connectCalls.mockClear();
|
|
166
|
+
mssqlMockState.closeCalls.mockClear();
|
|
167
|
+
mssqlMockState.requestCalls.mockClear();
|
|
168
|
+
});
|
|
169
|
+
afterEach(() => {
|
|
170
|
+
process.env = originalEnv;
|
|
171
|
+
});
|
|
172
|
+
it("returns a helpful error when no database access is configured", async () => {
|
|
173
|
+
delete process.env.DATABASE_NAME;
|
|
174
|
+
delete process.env.DATABASES;
|
|
175
|
+
const result = await getSqlRequest();
|
|
176
|
+
expect(result.error).toBe("Invalid or disallowed database. Set DATABASE_NAME or DATABASES to configure database access. Use the databaseName parameter to target a specific configured database.");
|
|
177
|
+
});
|
|
178
|
+
it("returns allowed databases in the error when a disallowed database is requested", async () => {
|
|
179
|
+
process.env.DATABASES = "ProdDB,StagingDB";
|
|
180
|
+
const result = await getSqlRequest("OtherDb");
|
|
181
|
+
expect(result.error).toBe("Invalid or disallowed database. Allowed: ProdDB, StagingDB. Use the databaseName parameter to target a specific configured database.");
|
|
182
|
+
});
|
|
183
|
+
it("returns a request using the resolved default database", async () => {
|
|
184
|
+
process.env.DATABASES = "ProdDBDefault,StagingDB";
|
|
185
|
+
delete process.env.DATABASE_NAME;
|
|
186
|
+
const result = await getSqlRequest();
|
|
187
|
+
expect(result.error).toBeUndefined();
|
|
188
|
+
expect(result.request).toBe(mssqlMockState.requestObject);
|
|
189
|
+
expect(mssqlMockState.poolConfigs).toContainEqual(expect.objectContaining({ database: "ProdDBDefault" }));
|
|
190
|
+
expect(mssqlMockState.connectCalls).toHaveBeenCalledWith(expect.objectContaining({ database: "ProdDBDefault" }));
|
|
191
|
+
expect(mssqlMockState.requestCalls).toHaveBeenCalledWith(expect.objectContaining({ database: "ProdDBDefault" }));
|
|
192
|
+
});
|
|
193
|
+
it("falls back to the first allowed database when DATABASE_NAME is disallowed", async () => {
|
|
194
|
+
process.env.DATABASES = "ProdDB,StagingDB";
|
|
195
|
+
process.env.DATABASE_NAME = "OtherDb";
|
|
196
|
+
const result = await getSqlRequest();
|
|
197
|
+
expect(result.error).toBeUndefined();
|
|
198
|
+
expect(result.request).toBe(mssqlMockState.requestObject);
|
|
199
|
+
expect(mssqlMockState.poolConfigs).toContainEqual(expect.objectContaining({ database: "ProdDB" }));
|
|
200
|
+
expect(mssqlMockState.connectCalls).toHaveBeenCalledWith(expect.objectContaining({ database: "ProdDB" }));
|
|
201
|
+
expect(mssqlMockState.requestCalls).toHaveBeenCalledWith(expect.objectContaining({ database: "ProdDB" }));
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
//# sourceMappingURL=db.test.js.map
|