ssms-mcp 0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
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,177 @@
1
+ # ssms-mcp
2
+
3
+ A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that lets AI assistants execute T-SQL queries against Microsoft SQL Server (the engine behind SSMS).
4
+
5
+ ## Features
6
+
7
+ Tools exposed over MCP (stdio):
8
+
9
+ | Tool | Description |
10
+ | --- | --- |
11
+ | `execute_query` | Run any T-SQL with optional named parameters. |
12
+ | `list_databases` | List user databases. |
13
+ | `list_tables` | List tables/views in a database (optional schema filter). |
14
+ | `describe_table` | Columns, types, nullability, defaults, and PK. |
15
+ | `server_info` | Server version + current connection context. |
16
+
17
+ Safety controls:
18
+
19
+ - `MSSQL_READ_ONLY=true` blocks `INSERT/UPDATE/DELETE/DDL/EXEC/...`.
20
+ - `MSSQL_MAX_ROWS` caps rows returned per recordset (default `1000`).
21
+
22
+ Authentication modes (set `MSSQL_AUTH_TYPE`):
23
+
24
+ | Value | Description |
25
+ | --- | --- |
26
+ | `sql` | SQL auth (`MSSQL_USER` / `MSSQL_PASSWORD`). |
27
+ | `ntlm` | Windows / NTLM (`MSSQL_DOMAIN` / `MSSQL_NTLM_USER` / `MSSQL_NTLM_PASSWORD`). |
28
+ | `entra-interactive` | **Microsoft Entra MFA via browser popup** — same flow SSMS uses. |
29
+ | `entra-device-code` | Microsoft Entra MFA via device-code flow (great for headless / remote shells). |
30
+ | `entra-default` | `DefaultAzureCredential` chain (Azure CLI, VS, env vars, MSI…). |
31
+ | `entra-password` | Entra username + password. **Does NOT support MFA.** |
32
+ | `entra-service-principal` | App registration client credentials. |
33
+ | `entra-msi` | Managed Identity (Azure VMs, App Service, etc.). |
34
+ | `entra-access-token` | Pre-supplied bearer token in `MSSQL_ACCESS_TOKEN`. |
35
+
36
+ Default: `sql` if `MSSQL_USER` is set, otherwise `ntlm` if `MSSQL_DOMAIN` is set, otherwise `entra-default`.
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ npm install -g ssms-mcp
42
+ ```
43
+
44
+ Or run with `npx` (no install):
45
+
46
+ ```bash
47
+ npx ssms-mcp
48
+ ```
49
+
50
+ ## Configure
51
+
52
+ Set environment variables before launching:
53
+
54
+ | Variable | Required | Default | Notes |
55
+ | --- | --- | --- | --- |
56
+ | `MSSQL_SERVER` | yes | — | `localhost`, `host\SQLEXPRESS`, FQDN, etc. |
57
+ | `MSSQL_DATABASE` | no | — | Default database. |
58
+ | `MSSQL_USER` | no | — | SQL auth user. Omit for Windows auth. |
59
+ | `MSSQL_PASSWORD` | no | — | SQL auth password. |
60
+ | `MSSQL_PORT` | no | 1433 | |
61
+ | `MSSQL_INSTANCE_NAME` | no | — | e.g. `SQLEXPRESS`. |
62
+ | `MSSQL_ENCRYPT` | no | `true` | |
63
+ | `MSSQL_TRUST_SERVER_CERTIFICATE` | no | `true` | Set `false` in production. |
64
+ | `MSSQL_CONNECT_TIMEOUT_MS` | no | 15000 | |
65
+ | `MSSQL_REQUEST_TIMEOUT_MS` | no | 30000 | |
66
+ | `MSSQL_READ_ONLY` | no | `false` | Block writes/DDL. |
67
+ | `MSSQL_MAX_ROWS` | no | 1000 | Per recordset cap. |
68
+ | `MSSQL_AUTH_TYPE` | no | auto | See table above. |
69
+ | `MSSQL_TENANT_ID` | optional* | — | Azure AD tenant id (or `common` / `organizations`). See note below. |
70
+ | `MSSQL_CLIENT_ID` | optional | Azure CLI public client | App registration client id. |
71
+ | `MSSQL_REDIRECT_URI` | entra-interactive | `http://localhost` | Must match app reg. |
72
+ | `MSSQL_ENTRA_USERNAME` | optional | — | Login hint / username for entra-password. |
73
+ | `MSSQL_ENTRA_PASSWORD` | entra-password | — | Password (no MFA). |
74
+ | `MSSQL_CLIENT_SECRET` | entra-service-principal | — | App reg secret. |
75
+ | `MSSQL_ACCESS_TOKEN` | entra-access-token | — | Bearer token for `https://database.windows.net`. |
76
+ | `MSSQL_DOMAIN` / `MSSQL_NTLM_USER` / `MSSQL_NTLM_PASSWORD` | ntlm | — | NTLM auth. |
77
+
78
+ \* `MSSQL_TENANT_ID` is **only required** for `entra-password` and `entra-service-principal`.
79
+ For `entra-interactive`, `entra-device-code`, and `entra-default` it can be omitted —
80
+ `@azure/identity` will default to the `organizations` tenant, which works for any
81
+ work/school account in its home tenant. Set it explicitly when:
82
+
83
+ - you are a **guest** in the target Azure SQL tenant, or
84
+ - you want to skip the account picker / pin auth to a specific tenant.
85
+
86
+ ## Use with VS Code / Claude Desktop
87
+
88
+ Add to your MCP client config (e.g. `claude_desktop_config.json` or VS Code MCP settings):
89
+
90
+ ```json
91
+ {
92
+ "mcpServers": {
93
+ "ssms": {
94
+ "command": "npx",
95
+ "args": ["-y", "ssms-mcp"],
96
+ "env": {
97
+ "MSSQL_SERVER": "localhost",
98
+ "MSSQL_DATABASE": "AdventureWorks",
99
+ "MSSQL_USER": "sa",
100
+ "MSSQL_PASSWORD": "your-password",
101
+ "MSSQL_READ_ONLY": "true"
102
+ }
103
+ }
104
+ }
105
+ }
106
+ ```
107
+
108
+ ### Azure SQL with Microsoft Entra MFA (browser popup)
109
+
110
+ This matches SSMS → "Microsoft Entra MFA". On first use, a browser window opens and you complete MFA; the token is cached in-memory until it expires.
111
+
112
+ ```json
113
+ {
114
+ "mcpServers": {
115
+ "ssms": {
116
+ "command": "npx",
117
+ "args": ["-y", "ssms-mcp"],
118
+ "env": {
119
+ "MSSQL_SERVER": "myserver.database.windows.net",
120
+ "MSSQL_DATABASE": "mydb",
121
+ "MSSQL_AUTH_TYPE": "entra-interactive",
122
+ "MSSQL_TENANT_ID": "<your-tenant-id-or-common>",
123
+ "MSSQL_ENTRA_USERNAME": "you@contoso.com",
124
+ "MSSQL_READ_ONLY": "true"
125
+ }
126
+ }
127
+ }
128
+ }
129
+ ```
130
+
131
+ Headless / remote machine? Use device code flow — the URL + code are printed to stderr:
132
+
133
+ ```json
134
+ "env": {
135
+ "MSSQL_SERVER": "myserver.database.windows.net",
136
+ "MSSQL_DATABASE": "mydb",
137
+ "MSSQL_AUTH_TYPE": "entra-device-code",
138
+ "MSSQL_TENANT_ID": "<tenant>"
139
+ }
140
+ ```
141
+
142
+ Already signed in via `az login` / Visual Studio? Just use the default chain:
143
+
144
+ ```json
145
+ "env": {
146
+ "MSSQL_SERVER": "myserver.database.windows.net",
147
+ "MSSQL_DATABASE": "mydb",
148
+ "MSSQL_AUTH_TYPE": "entra-default"
149
+ }
150
+ ```
151
+
152
+ ## Example tool calls
153
+
154
+ ```jsonc
155
+ // execute_query
156
+ {
157
+ "query": "SELECT TOP (@n) name FROM sys.tables WHERE name LIKE @pattern",
158
+ "parameters": { "n": 5, "pattern": "Sales%" }
159
+ }
160
+ ```
161
+
162
+ ```jsonc
163
+ // describe_table
164
+ { "table": "dbo.Customer", "database": "Sales" }
165
+ ```
166
+
167
+ ## Develop
168
+
169
+ ```bash
170
+ npm install
171
+ npm run build
172
+ npm start
173
+ ```
174
+
175
+ ## License
176
+
177
+ MIT
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SSMS MCP Server
4
+ * -----------------
5
+ * A Model Context Protocol server that exposes tools for executing
6
+ * T-SQL queries and inspecting schemas on Microsoft SQL Server
7
+ * (the engine behind SSMS).
8
+ *
9
+ * Connection is configured via environment variables:
10
+ * MSSQL_SERVER (required) e.g. "localhost" or "myhost\\SQLEXPRESS" or "myserver.database.windows.net"
11
+ * MSSQL_DATABASE (optional) default database
12
+ * MSSQL_AUTH_TYPE (optional) one of:
13
+ * "sql" SQL auth (MSSQL_USER/PASSWORD)
14
+ * "ntlm" Windows / NTLM (MSSQL_DOMAIN/MSSQL_NTLM_USER/PASSWORD)
15
+ * "entra-interactive" Microsoft Entra MFA via browser popup (SSMS-style)
16
+ * "entra-device-code" Microsoft Entra MFA via device-code flow
17
+ * "entra-default" DefaultAzureCredential chain (CLI/VS/MSI/env)
18
+ * "entra-password" Entra username+password (NOT MFA)
19
+ * "entra-service-principal" Entra client credentials
20
+ * "entra-msi" Managed Identity
21
+ * "entra-access-token" Pre-supplied token (MSSQL_ACCESS_TOKEN)
22
+ * Default: "sql" if MSSQL_USER set, else "ntlm" if MSSQL_DOMAIN set, else "entra-default".
23
+ * MSSQL_USER (optional) SQL auth user
24
+ * MSSQL_PASSWORD (optional) SQL auth password
25
+ * MSSQL_PORT (optional) default 1433
26
+ * MSSQL_ENCRYPT (optional) "true"|"false", default "true"
27
+ * MSSQL_TRUST_SERVER_CERTIFICATE (optional) "true"|"false", default "true"
28
+ * MSSQL_INSTANCE_NAME (optional) named instance, e.g. "SQLEXPRESS"
29
+ * MSSQL_CONNECT_TIMEOUT_MS (optional) default 15000
30
+ * MSSQL_REQUEST_TIMEOUT_MS (optional) default 30000
31
+ * MSSQL_READ_ONLY (optional) "true" to reject non-SELECT statements
32
+ * MSSQL_MAX_ROWS (optional) cap rows returned per query (default 1000)
33
+ *
34
+ * Entra-specific env vars:
35
+ * MSSQL_TENANT_ID Azure AD tenant id (or "common" / "organizations")
36
+ * MSSQL_CLIENT_ID App registration client id (defaults to Azure CLI public client)
37
+ * MSSQL_REDIRECT_URI For entra-interactive (default "http://localhost")
38
+ * MSSQL_ENTRA_USERNAME For entra-password / hint for entra-interactive
39
+ * MSSQL_ENTRA_PASSWORD For entra-password (no MFA)
40
+ * MSSQL_CLIENT_SECRET For entra-service-principal
41
+ * MSSQL_ACCESS_TOKEN For entra-access-token (raw bearer token for https://database.windows.net)
42
+ */
43
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,506 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SSMS MCP Server
4
+ * -----------------
5
+ * A Model Context Protocol server that exposes tools for executing
6
+ * T-SQL queries and inspecting schemas on Microsoft SQL Server
7
+ * (the engine behind SSMS).
8
+ *
9
+ * Connection is configured via environment variables:
10
+ * MSSQL_SERVER (required) e.g. "localhost" or "myhost\\SQLEXPRESS" or "myserver.database.windows.net"
11
+ * MSSQL_DATABASE (optional) default database
12
+ * MSSQL_AUTH_TYPE (optional) one of:
13
+ * "sql" SQL auth (MSSQL_USER/PASSWORD)
14
+ * "ntlm" Windows / NTLM (MSSQL_DOMAIN/MSSQL_NTLM_USER/PASSWORD)
15
+ * "entra-interactive" Microsoft Entra MFA via browser popup (SSMS-style)
16
+ * "entra-device-code" Microsoft Entra MFA via device-code flow
17
+ * "entra-default" DefaultAzureCredential chain (CLI/VS/MSI/env)
18
+ * "entra-password" Entra username+password (NOT MFA)
19
+ * "entra-service-principal" Entra client credentials
20
+ * "entra-msi" Managed Identity
21
+ * "entra-access-token" Pre-supplied token (MSSQL_ACCESS_TOKEN)
22
+ * Default: "sql" if MSSQL_USER set, else "ntlm" if MSSQL_DOMAIN set, else "entra-default".
23
+ * MSSQL_USER (optional) SQL auth user
24
+ * MSSQL_PASSWORD (optional) SQL auth password
25
+ * MSSQL_PORT (optional) default 1433
26
+ * MSSQL_ENCRYPT (optional) "true"|"false", default "true"
27
+ * MSSQL_TRUST_SERVER_CERTIFICATE (optional) "true"|"false", default "true"
28
+ * MSSQL_INSTANCE_NAME (optional) named instance, e.g. "SQLEXPRESS"
29
+ * MSSQL_CONNECT_TIMEOUT_MS (optional) default 15000
30
+ * MSSQL_REQUEST_TIMEOUT_MS (optional) default 30000
31
+ * MSSQL_READ_ONLY (optional) "true" to reject non-SELECT statements
32
+ * MSSQL_MAX_ROWS (optional) cap rows returned per query (default 1000)
33
+ *
34
+ * Entra-specific env vars:
35
+ * MSSQL_TENANT_ID Azure AD tenant id (or "common" / "organizations")
36
+ * MSSQL_CLIENT_ID App registration client id (defaults to Azure CLI public client)
37
+ * MSSQL_REDIRECT_URI For entra-interactive (default "http://localhost")
38
+ * MSSQL_ENTRA_USERNAME For entra-password / hint for entra-interactive
39
+ * MSSQL_ENTRA_PASSWORD For entra-password (no MFA)
40
+ * MSSQL_CLIENT_SECRET For entra-service-principal
41
+ * MSSQL_ACCESS_TOKEN For entra-access-token (raw bearer token for https://database.windows.net)
42
+ */
43
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
44
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
45
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
46
+ import sql from "mssql";
47
+ import { ClientSecretCredential, DefaultAzureCredential, DeviceCodeCredential, InteractiveBrowserCredential, ManagedIdentityCredential, UsernamePasswordCredential, } from "@azure/identity";
48
+ // ---------------------------------------------------------------------------
49
+ // Config
50
+ // ---------------------------------------------------------------------------
51
+ function envBool(name, def) {
52
+ const v = process.env[name];
53
+ if (v === undefined)
54
+ return def;
55
+ return /^(1|true|yes|on)$/i.test(v);
56
+ }
57
+ function envInt(name, def) {
58
+ const v = process.env[name];
59
+ if (!v)
60
+ return def;
61
+ const n = Number.parseInt(v, 10);
62
+ return Number.isFinite(n) ? n : def;
63
+ }
64
+ const READ_ONLY = envBool("MSSQL_READ_ONLY", false);
65
+ const MAX_ROWS = envInt("MSSQL_MAX_ROWS", 1000);
66
+ // SQL Server resource scope for Entra access tokens.
67
+ const SQL_SCOPE = "https://database.windows.net/.default";
68
+ // Azure CLI's public client id. Works for interactive/device-code against any
69
+ // tenant that allows it. Override with MSSQL_CLIENT_ID for your own app reg.
70
+ const DEFAULT_PUBLIC_CLIENT_ID = "04b07795-8ddb-461a-bbee-02f9e1bf7b46";
71
+ function resolveAuthType() {
72
+ const explicit = process.env.MSSQL_AUTH_TYPE?.toLowerCase().trim();
73
+ if (explicit)
74
+ return explicit;
75
+ if (process.env.MSSQL_USER)
76
+ return "sql";
77
+ if (process.env.MSSQL_DOMAIN)
78
+ return "ntlm";
79
+ return "entra-default";
80
+ }
81
+ function buildEntraCredential(authType) {
82
+ const tenantId = process.env.MSSQL_TENANT_ID;
83
+ const clientId = process.env.MSSQL_CLIENT_ID ?? DEFAULT_PUBLIC_CLIENT_ID;
84
+ switch (authType) {
85
+ case "entra-interactive":
86
+ return new InteractiveBrowserCredential({
87
+ tenantId,
88
+ clientId,
89
+ redirectUri: process.env.MSSQL_REDIRECT_URI ?? "http://localhost",
90
+ loginHint: process.env.MSSQL_ENTRA_USERNAME,
91
+ });
92
+ case "entra-device-code":
93
+ return new DeviceCodeCredential({
94
+ tenantId,
95
+ clientId,
96
+ userPromptCallback: (info) => {
97
+ // stderr only — stdout is reserved for MCP framing.
98
+ console.error(`[ssms-mcp] ${info.message}`);
99
+ },
100
+ });
101
+ case "entra-password": {
102
+ const username = process.env.MSSQL_ENTRA_USERNAME;
103
+ const password = process.env.MSSQL_ENTRA_PASSWORD;
104
+ if (!tenantId || !username || !password) {
105
+ throw new Error("entra-password requires MSSQL_TENANT_ID, MSSQL_ENTRA_USERNAME and MSSQL_ENTRA_PASSWORD. Note: this flow does NOT support MFA.");
106
+ }
107
+ return new UsernamePasswordCredential(tenantId, clientId, username, password);
108
+ }
109
+ case "entra-service-principal": {
110
+ const secret = process.env.MSSQL_CLIENT_SECRET;
111
+ if (!tenantId || !process.env.MSSQL_CLIENT_ID || !secret) {
112
+ throw new Error("entra-service-principal requires MSSQL_TENANT_ID, MSSQL_CLIENT_ID and MSSQL_CLIENT_SECRET.");
113
+ }
114
+ return new ClientSecretCredential(tenantId, process.env.MSSQL_CLIENT_ID, secret);
115
+ }
116
+ case "entra-msi":
117
+ return new ManagedIdentityCredential(process.env.MSSQL_CLIENT_ID ? { clientId: process.env.MSSQL_CLIENT_ID } : undefined);
118
+ case "entra-default":
119
+ default:
120
+ return new DefaultAzureCredential({ tenantId });
121
+ }
122
+ }
123
+ // Cache acquired tokens so we don't trigger a fresh browser prompt for every
124
+ // pool. Refresh ~5 minutes before expiry.
125
+ let cachedToken = null;
126
+ let credential = null;
127
+ let credentialAuthType = null;
128
+ async function acquireEntraToken(authType) {
129
+ if (authType === "entra-access-token") {
130
+ const t = process.env.MSSQL_ACCESS_TOKEN;
131
+ if (!t) {
132
+ throw new Error("entra-access-token requires MSSQL_ACCESS_TOKEN (a bearer token for https://database.windows.net).");
133
+ }
134
+ return t;
135
+ }
136
+ const now = Date.now();
137
+ if (cachedToken &&
138
+ credentialAuthType === authType &&
139
+ cachedToken.expiresOnTimestamp - now > 5 * 60 * 1000) {
140
+ return cachedToken.token;
141
+ }
142
+ if (!credential || credentialAuthType !== authType) {
143
+ credential = buildEntraCredential(authType);
144
+ credentialAuthType = authType;
145
+ cachedToken = null;
146
+ }
147
+ const tok = await credential.getToken(SQL_SCOPE);
148
+ if (!tok) {
149
+ throw new Error(`Failed to acquire Entra access token via ${authType}.`);
150
+ }
151
+ cachedToken = tok;
152
+ return tok.token;
153
+ }
154
+ function buildPoolConfig(databaseOverride) {
155
+ const server = process.env.MSSQL_SERVER;
156
+ if (!server) {
157
+ throw new Error("MSSQL_SERVER environment variable is required (e.g. 'localhost' or 'host\\\\SQLEXPRESS').");
158
+ }
159
+ const config = {
160
+ server,
161
+ database: databaseOverride ?? process.env.MSSQL_DATABASE,
162
+ port: process.env.MSSQL_PORT ? Number(process.env.MSSQL_PORT) : undefined,
163
+ connectionTimeout: envInt("MSSQL_CONNECT_TIMEOUT_MS", 15000),
164
+ requestTimeout: envInt("MSSQL_REQUEST_TIMEOUT_MS", 30000),
165
+ options: {
166
+ encrypt: envBool("MSSQL_ENCRYPT", true),
167
+ trustServerCertificate: envBool("MSSQL_TRUST_SERVER_CERTIFICATE", true),
168
+ instanceName: process.env.MSSQL_INSTANCE_NAME,
169
+ },
170
+ pool: { max: 5, min: 0, idleTimeoutMillis: 30000 },
171
+ };
172
+ return config;
173
+ }
174
+ async function buildAuthenticatedConfig(databaseOverride) {
175
+ const config = buildPoolConfig(databaseOverride);
176
+ const authType = resolveAuthType();
177
+ if (authType === "sql") {
178
+ const user = process.env.MSSQL_USER;
179
+ if (!user) {
180
+ throw new Error("MSSQL_AUTH_TYPE=sql requires MSSQL_USER (and MSSQL_PASSWORD).");
181
+ }
182
+ config.user = user;
183
+ config.password = process.env.MSSQL_PASSWORD ?? "";
184
+ config.authentication = {
185
+ type: "default",
186
+ options: { userName: user, password: process.env.MSSQL_PASSWORD ?? "" },
187
+ };
188
+ return config;
189
+ }
190
+ if (authType === "ntlm") {
191
+ const domain = process.env.MSSQL_DOMAIN;
192
+ if (!domain) {
193
+ throw new Error("MSSQL_AUTH_TYPE=ntlm requires MSSQL_DOMAIN.");
194
+ }
195
+ config.authentication = {
196
+ type: "ntlm",
197
+ options: {
198
+ domain,
199
+ userName: process.env.MSSQL_NTLM_USER ?? "",
200
+ password: process.env.MSSQL_NTLM_PASSWORD ?? "",
201
+ },
202
+ };
203
+ return config;
204
+ }
205
+ // All entra-* variants resolve to a pre-acquired bearer token that tedious
206
+ // accepts via 'azure-active-directory-access-token'.
207
+ const token = await acquireEntraToken(authType);
208
+ config.authentication = {
209
+ type: "azure-active-directory-access-token",
210
+ options: { token },
211
+ };
212
+ // Encryption is required for Azure SQL / Entra auth.
213
+ config.options = { ...config.options, encrypt: true };
214
+ return config;
215
+ }
216
+ // ---------------------------------------------------------------------------
217
+ // Connection pool cache (per database)
218
+ // ---------------------------------------------------------------------------
219
+ const pools = new Map();
220
+ async function getPool(database) {
221
+ const key = database ?? process.env.MSSQL_DATABASE ?? "__default__";
222
+ const existing = pools.get(key);
223
+ if (existing)
224
+ return existing;
225
+ const created = (async () => {
226
+ const cfg = await buildAuthenticatedConfig(database);
227
+ return new sql.ConnectionPool(cfg).connect();
228
+ })();
229
+ pools.set(key, created);
230
+ created.catch(() => pools.delete(key));
231
+ return created;
232
+ }
233
+ async function closeAllPools() {
234
+ const all = Array.from(pools.values());
235
+ pools.clear();
236
+ await Promise.allSettled(all.map(async (p) => {
237
+ try {
238
+ const pool = await p;
239
+ await pool.close();
240
+ }
241
+ catch {
242
+ /* ignore */
243
+ }
244
+ }));
245
+ }
246
+ // ---------------------------------------------------------------------------
247
+ // Helpers
248
+ // ---------------------------------------------------------------------------
249
+ const WRITE_REGEX = /\b(INSERT|UPDATE|DELETE|MERGE|DROP|TRUNCATE|ALTER|CREATE|GRANT|REVOKE|EXEC(UTE)?|BACKUP|RESTORE)\b/i;
250
+ function assertReadOnly(query) {
251
+ if (READ_ONLY && WRITE_REGEX.test(query)) {
252
+ throw new Error("Server is in READ_ONLY mode (MSSQL_READ_ONLY=true); write/DDL statements are blocked.");
253
+ }
254
+ }
255
+ function trimRows(rows) {
256
+ if (rows.length > MAX_ROWS) {
257
+ return { rows: rows.slice(0, MAX_ROWS), truncated: true };
258
+ }
259
+ return { rows, truncated: false };
260
+ }
261
+ function asTextResult(payload) {
262
+ return {
263
+ content: [
264
+ {
265
+ type: "text",
266
+ text: typeof payload === "string"
267
+ ? payload
268
+ : JSON.stringify(payload, null, 2),
269
+ },
270
+ ],
271
+ };
272
+ }
273
+ function asErrorResult(err) {
274
+ const msg = err instanceof Error ? err.message : String(err);
275
+ return {
276
+ isError: true,
277
+ content: [{ type: "text", text: `Error: ${msg}` }],
278
+ };
279
+ }
280
+ // ---------------------------------------------------------------------------
281
+ // Tool definitions
282
+ // ---------------------------------------------------------------------------
283
+ const TOOLS = [
284
+ {
285
+ name: "execute_query",
286
+ description: "Execute an arbitrary T-SQL query (or batch) against SQL Server and return the result rows as JSON. Honors MSSQL_READ_ONLY and MSSQL_MAX_ROWS.",
287
+ inputSchema: {
288
+ type: "object",
289
+ properties: {
290
+ query: { type: "string", description: "The T-SQL to execute." },
291
+ database: {
292
+ type: "string",
293
+ description: "Optional database to execute against (overrides default).",
294
+ },
295
+ parameters: {
296
+ type: "object",
297
+ description: "Optional named parameters: { name: value }. Reference them in SQL as @name.",
298
+ additionalProperties: true,
299
+ },
300
+ },
301
+ required: ["query"],
302
+ },
303
+ },
304
+ {
305
+ name: "list_databases",
306
+ description: "List user databases on the server (excludes system DBs).",
307
+ inputSchema: { type: "object", properties: {} },
308
+ },
309
+ {
310
+ name: "list_tables",
311
+ description: "List tables (and views) in a database. Returns schema, name, and type.",
312
+ inputSchema: {
313
+ type: "object",
314
+ properties: {
315
+ database: { type: "string" },
316
+ schema: {
317
+ type: "string",
318
+ description: "Optional schema filter (e.g. 'dbo').",
319
+ },
320
+ },
321
+ },
322
+ },
323
+ {
324
+ name: "describe_table",
325
+ description: "Describe a table: columns, types, nullability, defaults, and primary key.",
326
+ inputSchema: {
327
+ type: "object",
328
+ properties: {
329
+ table: {
330
+ type: "string",
331
+ description: "Table name. May be 'schema.table' or just 'table'.",
332
+ },
333
+ database: { type: "string" },
334
+ },
335
+ required: ["table"],
336
+ },
337
+ },
338
+ {
339
+ name: "server_info",
340
+ description: "Return SQL Server version and current connection context.",
341
+ inputSchema: { type: "object", properties: {} },
342
+ },
343
+ ];
344
+ // ---------------------------------------------------------------------------
345
+ // Tool handlers
346
+ // ---------------------------------------------------------------------------
347
+ async function handleExecuteQuery(args) {
348
+ const { query, database, parameters } = args;
349
+ if (!query || typeof query !== "string") {
350
+ throw new Error("`query` must be a non-empty string.");
351
+ }
352
+ assertReadOnly(query);
353
+ const pool = await getPool(database);
354
+ const request = pool.request();
355
+ if (parameters && typeof parameters === "object") {
356
+ for (const [k, v] of Object.entries(parameters)) {
357
+ request.input(k, v);
358
+ }
359
+ }
360
+ const result = await request.query(query);
361
+ // Multiple recordsets are possible.
362
+ const recordsets = result.recordsets ?? [];
363
+ const trimmed = recordsets.map((rs) => trimRows(rs));
364
+ return asTextResult({
365
+ rowsAffected: result.rowsAffected,
366
+ recordsetCount: recordsets.length,
367
+ recordsets: trimmed.map((t) => t.rows),
368
+ truncated: trimmed.some((t) => t.truncated),
369
+ maxRows: MAX_ROWS,
370
+ });
371
+ }
372
+ async function handleListDatabases() {
373
+ const pool = await getPool();
374
+ const result = await pool.request().query(`
375
+ SELECT name, database_id, create_date, collation_name, state_desc
376
+ FROM sys.databases
377
+ WHERE database_id > 4
378
+ ORDER BY name;
379
+ `);
380
+ return asTextResult(result.recordset);
381
+ }
382
+ async function handleListTables(args) {
383
+ const pool = await getPool(args.database);
384
+ const req = pool.request();
385
+ let where = "";
386
+ if (args.schema) {
387
+ req.input("schema", sql.NVarChar, args.schema);
388
+ where = "WHERE TABLE_SCHEMA = @schema";
389
+ }
390
+ const result = await req.query(`
391
+ SELECT TABLE_SCHEMA AS [schema], TABLE_NAME AS [name], TABLE_TYPE AS [type]
392
+ FROM INFORMATION_SCHEMA.TABLES
393
+ ${where}
394
+ ORDER BY TABLE_SCHEMA, TABLE_NAME;
395
+ `);
396
+ return asTextResult(result.recordset);
397
+ }
398
+ async function handleDescribeTable(args) {
399
+ const [schemaPart, tablePart] = args.table.includes(".")
400
+ ? args.table.split(".", 2)
401
+ : ["dbo", args.table];
402
+ const pool = await getPool(args.database);
403
+ const cols = await pool
404
+ .request()
405
+ .input("schema", sql.NVarChar, schemaPart)
406
+ .input("table", sql.NVarChar, tablePart).query(`
407
+ SELECT
408
+ COLUMN_NAME AS name,
409
+ DATA_TYPE AS dataType,
410
+ CHARACTER_MAXIMUM_LENGTH AS maxLength,
411
+ NUMERIC_PRECISION AS precision,
412
+ NUMERIC_SCALE AS scale,
413
+ IS_NULLABLE AS isNullable,
414
+ COLUMN_DEFAULT AS defaultValue,
415
+ ORDINAL_POSITION AS ordinal
416
+ FROM INFORMATION_SCHEMA.COLUMNS
417
+ WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = @table
418
+ ORDER BY ORDINAL_POSITION;
419
+ `);
420
+ const pk = await pool
421
+ .request()
422
+ .input("schema", sql.NVarChar, schemaPart)
423
+ .input("table", sql.NVarChar, tablePart).query(`
424
+ SELECT kcu.COLUMN_NAME AS name, kcu.ORDINAL_POSITION AS ordinal
425
+ FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
426
+ JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
427
+ ON tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME
428
+ AND tc.TABLE_SCHEMA = kcu.TABLE_SCHEMA
429
+ WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
430
+ AND tc.TABLE_SCHEMA = @schema
431
+ AND tc.TABLE_NAME = @table
432
+ ORDER BY kcu.ORDINAL_POSITION;
433
+ `);
434
+ return asTextResult({
435
+ schema: schemaPart,
436
+ table: tablePart,
437
+ columns: cols.recordset,
438
+ primaryKey: pk.recordset.map((r) => r.name),
439
+ });
440
+ }
441
+ async function handleServerInfo() {
442
+ const pool = await getPool();
443
+ const result = await pool.request().query(`
444
+ SELECT
445
+ @@VERSION AS version,
446
+ @@SERVERNAME AS serverName,
447
+ DB_NAME() AS currentDatabase,
448
+ SUSER_SNAME() AS loginName,
449
+ SYSTEM_USER AS systemUser;
450
+ `);
451
+ return asTextResult({
452
+ ...result.recordset[0],
453
+ authType: resolveAuthType(),
454
+ readOnly: READ_ONLY,
455
+ maxRows: MAX_ROWS,
456
+ });
457
+ }
458
+ // ---------------------------------------------------------------------------
459
+ // Server
460
+ // ---------------------------------------------------------------------------
461
+ const server = new Server({ name: "ssms-mcp", version: "0.2.0" }, { capabilities: { tools: {} } });
462
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
463
+ tools: TOOLS,
464
+ }));
465
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
466
+ const { name, arguments: args = {} } = req.params;
467
+ try {
468
+ switch (name) {
469
+ case "execute_query":
470
+ return await handleExecuteQuery(args);
471
+ case "list_databases":
472
+ return await handleListDatabases();
473
+ case "list_tables":
474
+ return await handleListTables(args);
475
+ case "describe_table":
476
+ return await handleDescribeTable(args);
477
+ case "server_info":
478
+ return await handleServerInfo();
479
+ default:
480
+ return asErrorResult(`Unknown tool: ${name}`);
481
+ }
482
+ }
483
+ catch (err) {
484
+ return asErrorResult(err);
485
+ }
486
+ });
487
+ async function main() {
488
+ const transport = new StdioServerTransport();
489
+ await server.connect(transport);
490
+ // Stderr is safe — stdout is reserved for MCP framing.
491
+ console.error("[ssms-mcp] server ready (stdio)");
492
+ }
493
+ const shutdown = async () => {
494
+ try {
495
+ await closeAllPools();
496
+ }
497
+ finally {
498
+ process.exit(0);
499
+ }
500
+ };
501
+ process.on("SIGINT", shutdown);
502
+ process.on("SIGTERM", shutdown);
503
+ main().catch((err) => {
504
+ console.error("[ssms-mcp] fatal:", err);
505
+ process.exit(1);
506
+ });
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "ssms-mcp",
3
+ "version": "0.2.0",
4
+ "description": "Model Context Protocol (MCP) server that executes SQL queries against Microsoft SQL Server (SSMS-compatible).",
5
+ "license": "MIT",
6
+ "author": "",
7
+ "type": "module",
8
+ "main": "dist/index.js",
9
+ "bin": {
10
+ "ssms-mcp": "dist/index.js"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "prepare": "npm run build",
20
+ "start": "node dist/index.js",
21
+ "dev": "tsc --watch"
22
+ },
23
+ "keywords": [
24
+ "mcp",
25
+ "model-context-protocol",
26
+ "ssms",
27
+ "sql-server",
28
+ "mssql",
29
+ "tsql",
30
+ "database",
31
+ "ai",
32
+ "llm"
33
+ ],
34
+ "engines": {
35
+ "node": ">=18"
36
+ },
37
+ "dependencies": {
38
+ "@azure/identity": "^4.4.1",
39
+ "@modelcontextprotocol/sdk": "^1.0.4",
40
+ "mssql": "^11.0.1",
41
+ "zod": "^3.23.8"
42
+ },
43
+ "devDependencies": {
44
+ "@types/mssql": "^9.1.5",
45
+ "@types/node": "^20.11.30",
46
+ "typescript": "^5.4.5"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ }
51
+ }