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 +21 -0
- package/README.md +177 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +506 -0
- package/package.json +51 -0
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
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|