@adriancy/mcp-mssql 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +38 -8
- package/README.md +75 -53
- package/dist/config.d.ts +23 -2
- package/dist/config.js +484 -42
- package/dist/index.js +25 -11
- package/dist/readonly-sql.d.ts +3 -1
- package/dist/readonly-sql.js +7 -7
- package/dist/schemas.js +1 -1
- package/package.json +6 -1
package/.env.example
CHANGED
|
@@ -1,21 +1,51 @@
|
|
|
1
1
|
# Required
|
|
2
2
|
MSSQL_SERVER=localhost
|
|
3
|
-
MSSQL_USER=
|
|
4
|
-
MSSQL_PASSWORD=
|
|
5
|
-
MSSQL_DATABASE=
|
|
3
|
+
MSSQL_USER=your_user
|
|
4
|
+
MSSQL_PASSWORD=your_password
|
|
5
|
+
MSSQL_DATABASE=your_database
|
|
6
6
|
|
|
7
|
-
# Optional (default 1433)
|
|
7
|
+
# Optional (default 1433). Omit or ignore when using MSSQL_INSTANCE_NAME (named instance).
|
|
8
8
|
# MSSQL_PORT=1433
|
|
9
9
|
|
|
10
|
-
# TLS (defaults: encrypt true, trust cert false)
|
|
10
|
+
# TLS (defaults: encrypt true, trust cert false). MSSQL_ENCRYPT may be true, false, or strict (TDS 8.0).
|
|
11
11
|
# MSSQL_ENCRYPT=true
|
|
12
12
|
# MSSQL_TRUST_SERVER_CERTIFICATE=false
|
|
13
|
+
# MSSQL_TLS_SERVER_NAME=
|
|
14
|
+
# Paths on the MCP host; PEM contents read at startup (fail fast if missing).
|
|
15
|
+
# MSSQL_TLS_CA_FILE=
|
|
16
|
+
# MSSQL_TLS_CERT_FILE=
|
|
17
|
+
# MSSQL_TLS_KEY_FILE=
|
|
18
|
+
# MSSQL_TLS_KEY_PASSPHRASE=
|
|
19
|
+
|
|
20
|
+
# Connectivity / resilience
|
|
21
|
+
# MSSQL_CONNECTION_TIMEOUT_MS=
|
|
22
|
+
# MSSQL_DOMAIN=
|
|
23
|
+
# MSSQL_INSTANCE_NAME=
|
|
24
|
+
# MSSQL_MULTI_SUBNET_FAILOVER=false
|
|
25
|
+
# MSSQL_READ_ONLY_INTENT=false
|
|
26
|
+
# MSSQL_MAX_RETRIES_ON_TRANSIENT_ERRORS=
|
|
27
|
+
# MSSQL_CONNECTION_RETRY_INTERVAL_MS=
|
|
28
|
+
|
|
29
|
+
# Pool (defaults: max 10, min 0, idle 30000 ms)
|
|
30
|
+
# MSSQL_POOL_MAX=10
|
|
31
|
+
# MSSQL_POOL_MIN=0
|
|
32
|
+
# MSSQL_POOL_IDLE_TIMEOUT_MS=30000
|
|
33
|
+
|
|
34
|
+
# Session / protocol
|
|
35
|
+
# MSSQL_APP_NAME=
|
|
36
|
+
# MSSQL_USE_UTC=true
|
|
37
|
+
# MSSQL_TDS_VERSION=
|
|
38
|
+
|
|
39
|
+
# Authentication (unset = SQL login via MSSQL_USER / MSSQL_PASSWORD).
|
|
40
|
+
# ntlm | azure-active-directory-password | azure-active-directory-access-token | azure-active-directory-service-principal-secret
|
|
41
|
+
# MSSQL_AUTH_TYPE=
|
|
42
|
+
# MSSQL_AZURE_CLIENT_ID=
|
|
43
|
+
# MSSQL_AZURE_TENANT_ID=
|
|
44
|
+
# MSSQL_AZURE_CLIENT_SECRET=
|
|
45
|
+
# MSSQL_AZURE_ACCESS_TOKEN=
|
|
13
46
|
|
|
14
47
|
# Safety: when false/ unset, blocks common write/DDL/exec patterns (heuristic only)
|
|
15
48
|
# MSSQL_ALLOW_WRITES=false
|
|
16
49
|
|
|
17
|
-
# Cap rows returned (SET ROWCOUNT); unset = no cap
|
|
18
|
-
# MSSQL_MAX_ROWS=5000
|
|
19
|
-
|
|
20
50
|
# Request timeout in ms (driver)
|
|
21
51
|
# MSSQL_QUERY_TIMEOUT_MS=30000
|
package/README.md
CHANGED
|
@@ -1,49 +1,20 @@
|
|
|
1
|
-
# mssql
|
|
1
|
+
# `@adriancy/mcp-mssql`
|
|
2
2
|
|
|
3
|
-
MCP server (stdio)
|
|
3
|
+
MCP server (stdio) for Microsoft SQL Server. Exposes tools so Cursor (or any MCP client) can list tables, describe columns, and run read-biased T-SQL.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Use it in Cursor
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- **`mssql_query`** — Run a T-SQL batch; returns `recordsets` and `rowsAffected`. Honors `MSSQL_MAX_ROWS` via `SET ROWCOUNT` when set.
|
|
10
|
-
- **`mssql_list_tables`** — Base tables from `INFORMATION_SCHEMA.TABLES`, optional schema filter.
|
|
11
|
-
- **`mssql_describe_table`** — Column metadata from `INFORMATION_SCHEMA.COLUMNS`.
|
|
12
|
-
|
|
13
|
-
## Environment variables
|
|
14
|
-
|
|
15
|
-
See [`.env.example`](.env.example). Required: `MSSQL_SERVER`, `MSSQL_USER`, `MSSQL_PASSWORD`, `MSSQL_DATABASE`.
|
|
16
|
-
|
|
17
|
-
- **`MSSQL_ALLOW_WRITES`** — Default off. When off, a heuristic blocks common write/DDL/exec keywords (not a substitute for DB permissions).
|
|
18
|
-
- **`MSSQL_MAX_ROWS`** — When set, wraps batches in `SET ROWCOUNT` for `mssql_query`.
|
|
19
|
-
- **`MSSQL_ENCRYPT`** / **`MSSQL_TRUST_SERVER_CERTIFICATE`** — Passed through to the driver (`encrypt` defaults to true).
|
|
20
|
-
|
|
21
|
-
## Build and run
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
pnpm install
|
|
25
|
-
pnpm run build
|
|
26
|
-
pnpm start
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
Development (no separate build):
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
pnpm dev
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Do not write logs to **stdout** when running under MCP; the protocol uses stdout. Errors on startup go to stderr via `console.error`.
|
|
7
|
+
1. Open **Cursor Settings → MCP** (or edit your MCP JSON).
|
|
8
|
+
2. Add a server block. Set `env` to your database (see [`.env.example`](.env.example)).
|
|
36
9
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
**Published package (`npx`):** from `@adriancy/mssql-mcp` **1.0.1+** you can run the CLI without a local path:
|
|
10
|
+
**From npm** (after the package is published):
|
|
40
11
|
|
|
41
12
|
```json
|
|
42
13
|
{
|
|
43
14
|
"mcpServers": {
|
|
44
15
|
"mssql": {
|
|
45
16
|
"command": "npx",
|
|
46
|
-
"args": ["-y", "@adriancy/mssql
|
|
17
|
+
"args": ["-y", "@adriancy/mcp-mssql"],
|
|
47
18
|
"env": {
|
|
48
19
|
"MSSQL_SERVER": "localhost",
|
|
49
20
|
"MSSQL_USER": "your_user",
|
|
@@ -56,14 +27,14 @@ Do not write logs to **stdout** when running under MCP; the protocol uses stdout
|
|
|
56
27
|
}
|
|
57
28
|
```
|
|
58
29
|
|
|
59
|
-
**
|
|
30
|
+
**From a local clone** (run `pnpm install && pnpm build` first):
|
|
60
31
|
|
|
61
32
|
```json
|
|
62
33
|
{
|
|
63
34
|
"mcpServers": {
|
|
64
35
|
"mssql": {
|
|
65
36
|
"command": "node",
|
|
66
|
-
"args": ["/
|
|
37
|
+
"args": ["/absolute/path/to/mssql-mcp/dist/index.js"],
|
|
67
38
|
"env": {
|
|
68
39
|
"MSSQL_SERVER": "localhost",
|
|
69
40
|
"MSSQL_USER": "your_user",
|
|
@@ -76,23 +47,74 @@ Do not write logs to **stdout** when running under MCP; the protocol uses stdout
|
|
|
76
47
|
}
|
|
77
48
|
```
|
|
78
49
|
|
|
79
|
-
Use **`node`** for local
|
|
50
|
+
Use **`node`** for local files. Avoid **`pnpm`** as the MCP `command` — extra output on stdout can break the protocol.
|
|
51
|
+
|
|
52
|
+
Confirm the install name matches npm: `npm view @adriancy/mcp-mssql version`.
|
|
53
|
+
|
|
54
|
+
## Environment
|
|
55
|
+
|
|
56
|
+
All variables are read from the MCP process environment (e.g. Cursor `env`). Booleans treat `1`, `true`, `yes`, `on` (case-insensitive) as true and `0`, `false`, `no`, `off` as false. Invalid boolean and integer values fail startup validation.
|
|
57
|
+
|
|
58
|
+
| Variable | Required | Default | Meaning |
|
|
59
|
+
|----------|----------|---------|---------|
|
|
60
|
+
| `MSSQL_SERVER` | yes | — | Hostname or IP of SQL Server |
|
|
61
|
+
| `MSSQL_USER` | yes† | — | Login user (SQL auth, NTLM, Azure AD password); not used for `azure-active-directory-access-token` or service-principal-only flows |
|
|
62
|
+
| `MSSQL_PASSWORD` | yes*† | — | Login password (*may be empty for some setups) |
|
|
63
|
+
| `MSSQL_DATABASE` | yes | — | Initial database |
|
|
64
|
+
| `MSSQL_PORT` | no | `1433` | TCP port; omit when using `MSSQL_INSTANCE_NAME` (driver uses instance + SQL Browser) |
|
|
65
|
+
| `MSSQL_ENCRYPT` | no | `true` | TLS `encrypt`: `true`, `false`, or `strict` (TDS 8.0 / tedious) |
|
|
66
|
+
| `MSSQL_TRUST_SERVER_CERTIFICATE` | no | `false` | Trust self-signed / skip cert validation (dev only) |
|
|
67
|
+
| `MSSQL_TLS_SERVER_NAME` | no | — | Hostname for TLS validation when it differs from `MSSQL_SERVER` (e.g. connect by IP) |
|
|
68
|
+
| `MSSQL_TLS_CA_FILE` | no | — | Path to CA PEM; passed via `cryptoCredentialsDetails.ca` |
|
|
69
|
+
| `MSSQL_TLS_CERT_FILE` | no | — | Optional client cert PEM (mutual TLS) |
|
|
70
|
+
| `MSSQL_TLS_KEY_FILE` | no | — | Optional client private key PEM |
|
|
71
|
+
| `MSSQL_TLS_KEY_PASSPHRASE` | no | — | Passphrase for encrypted client key |
|
|
72
|
+
| `MSSQL_CONNECTION_TIMEOUT_MS` | no | *(driver default)* | Pool `connectionTimeout` (ms). `0` → unset |
|
|
73
|
+
| `MSSQL_DOMAIN` | no | — | Domain login (`config.domain` for SQL auth; required for `MSSQL_AUTH_TYPE=ntlm`) |
|
|
74
|
+
| `MSSQL_INSTANCE_NAME` | no | — | Named instance (`options.instanceName`); do not rely on `MSSQL_PORT` with this set |
|
|
75
|
+
| `MSSQL_MULTI_SUBNET_FAILOVER` | no | `false` | Availability-group style failover hint |
|
|
76
|
+
| `MSSQL_READ_ONLY_INTENT` | no | `false` | Read-only routing for AG secondaries |
|
|
77
|
+
| `MSSQL_MAX_RETRIES_ON_TRANSIENT_ERRORS` | no | — | Tedious `maxRetriesOnTransientErrors`; `0` → unset |
|
|
78
|
+
| `MSSQL_CONNECTION_RETRY_INTERVAL_MS` | no | — | Tedious `connectionRetryInterval`; `0` → unset |
|
|
79
|
+
| `MSSQL_POOL_MAX` | no | `10` | Pool max connections |
|
|
80
|
+
| `MSSQL_POOL_MIN` | no | `0` | Pool min connections |
|
|
81
|
+
| `MSSQL_POOL_IDLE_TIMEOUT_MS` | no | `30000` | `pool.idleTimeoutMillis` |
|
|
82
|
+
| `MSSQL_APP_NAME` | no | — | `options.appName` (server tracing) |
|
|
83
|
+
| `MSSQL_USE_UTC` | no | `true` | `options.useUTC` when set |
|
|
84
|
+
| `MSSQL_TDS_VERSION` | no | — | `7_1`, `7_2`, `7_3_A`, `7_3_B`, or `7_4` |
|
|
85
|
+
| `MSSQL_AUTH_TYPE` | no | — | `ntlm`, `azure-active-directory-password`, `azure-active-directory-access-token`, `azure-active-directory-service-principal-secret`, or unset/`default` for SQL login |
|
|
86
|
+
| `MSSQL_AZURE_CLIENT_ID` | no‡ | — | Azure AD app (client) ID where required |
|
|
87
|
+
| `MSSQL_AZURE_TENANT_ID` | no‡ | — | Tenant ID (optional for password auth, defaults `common`; required for service principal) |
|
|
88
|
+
| `MSSQL_AZURE_CLIENT_SECRET` | no‡ | — | Service principal secret |
|
|
89
|
+
| `MSSQL_AZURE_ACCESS_TOKEN` | no‡ | — | Pre-obtained token for access-token auth |
|
|
90
|
+
| `MSSQL_ALLOW_WRITES` | no | `false` | If false/unset, blocks common write/DDL/exec patterns in `mssql_query` (heuristic only) |
|
|
91
|
+
| `MSSQL_QUERY_TIMEOUT_MS` | no | *(driver default)* | Request timeout in ms for the driver. `0` → unset |
|
|
92
|
+
|
|
93
|
+
† Required for SQL authentication and for auth types that use interactive user login.
|
|
94
|
+
‡ Required depending on `MSSQL_AUTH_TYPE`; see [`.env.example`](.env.example). NTLM on Node 17+ may need `--openssl-legacy-provider` (see [tedious FAQ](https://tediousjs.github.io/tedious/frequently-encountered-problems.html)).
|
|
95
|
+
|
|
96
|
+
See also [`.env.example`](.env.example).
|
|
80
97
|
|
|
81
|
-
|
|
98
|
+
## Tools
|
|
82
99
|
|
|
83
|
-
|
|
100
|
+
| Tool | Purpose |
|
|
101
|
+
|------|--------|
|
|
102
|
+
| `mssql_query` | Run T-SQL; returns rows and `rowsAffected` |
|
|
103
|
+
| `mssql_list_tables` | Tables in `INFORMATION_SCHEMA` |
|
|
104
|
+
| `mssql_describe_table` | Column metadata |
|
|
84
105
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
106
|
+
When `MSSQL_ALLOW_WRITES` is unset/false, obvious write/DDL patterns are blocked (heuristic only; use DB permissions for real safety).
|
|
107
|
+
|
|
108
|
+
## Develop
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
pnpm install
|
|
112
|
+
pnpm check
|
|
113
|
+
pnpm test
|
|
114
|
+
pnpm build && pnpm start
|
|
115
|
+
pnpm dev
|
|
96
116
|
```
|
|
97
117
|
|
|
98
|
-
|
|
118
|
+
Use `pnpm format` to apply Biome formatting fixes and `pnpm lint` to run lint rules without formatting.
|
|
119
|
+
|
|
120
|
+
Use a read-only or least-privilege SQL login for day-to-day use.
|
package/dist/config.d.ts
CHANGED
|
@@ -1,14 +1,35 @@
|
|
|
1
|
+
import type * as tls from 'node:tls';
|
|
2
|
+
export type AuthTypeNormalized = undefined | 'ntlm' | 'azure-active-directory-password' | 'azure-active-directory-access-token' | 'azure-active-directory-service-principal-secret';
|
|
1
3
|
export type AppConfig = {
|
|
2
4
|
server: string;
|
|
3
5
|
port: number;
|
|
4
6
|
user: string;
|
|
5
7
|
password: string;
|
|
6
8
|
database: string;
|
|
7
|
-
encrypt: boolean;
|
|
9
|
+
encrypt: boolean | 'strict';
|
|
8
10
|
trustServerCertificate: boolean;
|
|
9
11
|
allowWrites: boolean;
|
|
10
|
-
maxRows: number | undefined;
|
|
11
12
|
queryTimeoutMs: number | undefined;
|
|
13
|
+
authType: AuthTypeNormalized;
|
|
14
|
+
domain: string | undefined;
|
|
15
|
+
tlsServerName: string | undefined;
|
|
16
|
+
cryptoCredentialsDetails: tls.SecureContextOptions | undefined;
|
|
17
|
+
connectionTimeoutMs: number | undefined;
|
|
18
|
+
instanceName: string | undefined;
|
|
19
|
+
multiSubnetFailover: boolean | undefined;
|
|
20
|
+
readOnlyIntent: boolean | undefined;
|
|
21
|
+
maxRetriesOnTransientErrors: number | undefined;
|
|
22
|
+
connectionRetryIntervalMs: number | undefined;
|
|
23
|
+
poolMax: number;
|
|
24
|
+
poolMin: number;
|
|
25
|
+
poolIdleTimeoutMs: number;
|
|
26
|
+
appName: string | undefined;
|
|
27
|
+
useUtc: boolean | undefined;
|
|
28
|
+
tdsVersion: string | undefined;
|
|
29
|
+
azureClientId: string | undefined;
|
|
30
|
+
azureTenantId: string | undefined;
|
|
31
|
+
azureClientSecret: string | undefined;
|
|
32
|
+
azureAccessToken: string | undefined;
|
|
12
33
|
};
|
|
13
34
|
export declare function loadConfig(): AppConfig;
|
|
14
35
|
export declare function mssqlDriverConfig(cfg: AppConfig): import('mssql').config;
|
package/dist/config.js
CHANGED
|
@@ -1,29 +1,386 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
1
3
|
import * as v from 'valibot';
|
|
4
|
+
const TRUE_VALUES = new Set(['1', 'true', 'yes', 'on']);
|
|
5
|
+
const FALSE_VALUES = new Set(['0', 'false', 'no', 'off']);
|
|
6
|
+
const INTEGER_PATTERN = /^-?\d+$/;
|
|
7
|
+
function isUnset(raw) {
|
|
8
|
+
return raw === undefined || raw.trim() === '';
|
|
9
|
+
}
|
|
2
10
|
function parseBool(raw, defaultValue) {
|
|
3
|
-
|
|
11
|
+
const trimmed = raw?.trim();
|
|
12
|
+
if (trimmed === undefined || trimmed === '')
|
|
4
13
|
return defaultValue;
|
|
5
|
-
|
|
14
|
+
const normalized = trimmed.toLowerCase();
|
|
15
|
+
if (TRUE_VALUES.has(normalized))
|
|
16
|
+
return true;
|
|
17
|
+
if (FALSE_VALUES.has(normalized))
|
|
18
|
+
return false;
|
|
19
|
+
throw new Error(`Invalid boolean value "${raw}".`);
|
|
6
20
|
}
|
|
7
|
-
function parseIntEnv(raw
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
21
|
+
function parseIntEnv(raw) {
|
|
22
|
+
const trimmed = raw?.trim();
|
|
23
|
+
if (trimmed === undefined || trimmed === '')
|
|
24
|
+
return undefined;
|
|
25
|
+
if (!INTEGER_PATTERN.test(trimmed)) {
|
|
26
|
+
throw new Error(`Invalid integer value "${raw}".`);
|
|
27
|
+
}
|
|
28
|
+
return Number.parseInt(trimmed, 10);
|
|
29
|
+
}
|
|
30
|
+
function parseEncrypt(raw) {
|
|
31
|
+
const trimmed = raw?.trim();
|
|
32
|
+
if (trimmed === undefined || trimmed === '')
|
|
33
|
+
return true;
|
|
34
|
+
const t = trimmed.toLowerCase();
|
|
35
|
+
if (t === 'strict')
|
|
36
|
+
return 'strict';
|
|
37
|
+
return parseBool(raw, true);
|
|
38
|
+
}
|
|
39
|
+
function normalizeAuthType(raw) {
|
|
40
|
+
const trimmed = raw?.trim();
|
|
41
|
+
if (trimmed === undefined || trimmed === '')
|
|
42
|
+
return undefined;
|
|
43
|
+
const t = trimmed.toLowerCase();
|
|
44
|
+
if (t === '' || t === 'default')
|
|
45
|
+
return undefined;
|
|
46
|
+
return t;
|
|
47
|
+
}
|
|
48
|
+
const TDS_VERSIONS = new Set(['7_1', '7_2', '7_3_A', '7_3_B', '7_4']);
|
|
49
|
+
const ALLOWED_AUTH = new Set([
|
|
50
|
+
'ntlm',
|
|
51
|
+
'azure-active-directory-password',
|
|
52
|
+
'azure-active-directory-access-token',
|
|
53
|
+
'azure-active-directory-service-principal-secret',
|
|
54
|
+
]);
|
|
55
|
+
function readPemFileOptional(label, filePath) {
|
|
56
|
+
const trimmed = filePath?.trim();
|
|
57
|
+
if (trimmed === undefined || trimmed === '')
|
|
58
|
+
return undefined;
|
|
59
|
+
const resolved = path.resolve(trimmed);
|
|
60
|
+
try {
|
|
61
|
+
return fs.readFileSync(resolved);
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
65
|
+
throw new Error(`Failed to read ${label} file at ${resolved}: ${msg}`);
|
|
66
|
+
}
|
|
12
67
|
}
|
|
13
68
|
const envSchema = v.object({
|
|
14
69
|
MSSQL_SERVER: v.pipe(v.string(), v.nonEmpty()),
|
|
15
|
-
MSSQL_USER: v.
|
|
16
|
-
MSSQL_PASSWORD: v.string(),
|
|
70
|
+
MSSQL_USER: v.optional(v.string()),
|
|
71
|
+
MSSQL_PASSWORD: v.optional(v.string()),
|
|
17
72
|
MSSQL_DATABASE: v.pipe(v.string(), v.nonEmpty()),
|
|
18
73
|
MSSQL_PORT: v.optional(v.string()),
|
|
19
74
|
MSSQL_ENCRYPT: v.optional(v.string()),
|
|
20
75
|
MSSQL_TRUST_SERVER_CERTIFICATE: v.optional(v.string()),
|
|
21
76
|
MSSQL_ALLOW_WRITES: v.optional(v.string()),
|
|
22
|
-
MSSQL_MAX_ROWS: v.optional(v.string()),
|
|
23
77
|
MSSQL_QUERY_TIMEOUT_MS: v.optional(v.string()),
|
|
78
|
+
MSSQL_TLS_SERVER_NAME: v.optional(v.string()),
|
|
79
|
+
MSSQL_TLS_CA_FILE: v.optional(v.string()),
|
|
80
|
+
MSSQL_TLS_CERT_FILE: v.optional(v.string()),
|
|
81
|
+
MSSQL_TLS_KEY_FILE: v.optional(v.string()),
|
|
82
|
+
MSSQL_TLS_KEY_PASSPHRASE: v.optional(v.string()),
|
|
83
|
+
MSSQL_CONNECTION_TIMEOUT_MS: v.optional(v.string()),
|
|
84
|
+
MSSQL_DOMAIN: v.optional(v.string()),
|
|
85
|
+
MSSQL_INSTANCE_NAME: v.optional(v.string()),
|
|
86
|
+
MSSQL_MULTI_SUBNET_FAILOVER: v.optional(v.string()),
|
|
87
|
+
MSSQL_READ_ONLY_INTENT: v.optional(v.string()),
|
|
88
|
+
MSSQL_MAX_RETRIES_ON_TRANSIENT_ERRORS: v.optional(v.string()),
|
|
89
|
+
MSSQL_CONNECTION_RETRY_INTERVAL_MS: v.optional(v.string()),
|
|
90
|
+
MSSQL_POOL_MAX: v.optional(v.string()),
|
|
91
|
+
MSSQL_POOL_MIN: v.optional(v.string()),
|
|
92
|
+
MSSQL_POOL_IDLE_TIMEOUT_MS: v.optional(v.string()),
|
|
93
|
+
MSSQL_APP_NAME: v.optional(v.string()),
|
|
94
|
+
MSSQL_USE_UTC: v.optional(v.string()),
|
|
95
|
+
MSSQL_TDS_VERSION: v.optional(v.string()),
|
|
96
|
+
MSSQL_AUTH_TYPE: v.optional(v.string()),
|
|
97
|
+
MSSQL_AZURE_CLIENT_ID: v.optional(v.string()),
|
|
98
|
+
MSSQL_AZURE_TENANT_ID: v.optional(v.string()),
|
|
99
|
+
MSSQL_AZURE_CLIENT_SECRET: v.optional(v.string()),
|
|
100
|
+
MSSQL_AZURE_ACCESS_TOKEN: v.optional(v.string()),
|
|
24
101
|
});
|
|
25
|
-
|
|
26
|
-
const
|
|
102
|
+
function inferAuthType(e) {
|
|
103
|
+
const t = normalizeAuthType(e.MSSQL_AUTH_TYPE);
|
|
104
|
+
if (t === undefined)
|
|
105
|
+
return undefined;
|
|
106
|
+
return t;
|
|
107
|
+
}
|
|
108
|
+
const BOOLEAN_ENV_KEYS = [
|
|
109
|
+
'MSSQL_TRUST_SERVER_CERTIFICATE',
|
|
110
|
+
'MSSQL_ALLOW_WRITES',
|
|
111
|
+
'MSSQL_MULTI_SUBNET_FAILOVER',
|
|
112
|
+
'MSSQL_READ_ONLY_INTENT',
|
|
113
|
+
'MSSQL_USE_UTC',
|
|
114
|
+
];
|
|
115
|
+
const INTEGER_ENV_RULES = [
|
|
116
|
+
{ key: 'MSSQL_PORT', min: 1, max: 65_535 },
|
|
117
|
+
{ key: 'MSSQL_QUERY_TIMEOUT_MS', min: 0 },
|
|
118
|
+
{ key: 'MSSQL_CONNECTION_TIMEOUT_MS', min: 0 },
|
|
119
|
+
{ key: 'MSSQL_MAX_RETRIES_ON_TRANSIENT_ERRORS', min: 0 },
|
|
120
|
+
{ key: 'MSSQL_CONNECTION_RETRY_INTERVAL_MS', min: 0 },
|
|
121
|
+
{ key: 'MSSQL_POOL_MAX', min: 1 },
|
|
122
|
+
{ key: 'MSSQL_POOL_MIN', min: 0 },
|
|
123
|
+
{ key: 'MSSQL_POOL_IDLE_TIMEOUT_MS', min: 0 },
|
|
124
|
+
];
|
|
125
|
+
function rawScalarChecks(e, addIssue) {
|
|
126
|
+
if (e.MSSQL_SERVER.trim() === '') {
|
|
127
|
+
addIssue({ message: 'MSSQL_SERVER is required and cannot be blank.' });
|
|
128
|
+
}
|
|
129
|
+
if (e.MSSQL_DATABASE.trim() === '') {
|
|
130
|
+
addIssue({ message: 'MSSQL_DATABASE is required and cannot be blank.' });
|
|
131
|
+
}
|
|
132
|
+
for (const key of BOOLEAN_ENV_KEYS) {
|
|
133
|
+
const raw = e[key];
|
|
134
|
+
if (isUnset(raw))
|
|
135
|
+
continue;
|
|
136
|
+
const normalized = raw.trim().toLowerCase();
|
|
137
|
+
if (!TRUE_VALUES.has(normalized) && !FALSE_VALUES.has(normalized)) {
|
|
138
|
+
addIssue({
|
|
139
|
+
message: `${key} must be one of: 1, true, yes, on, 0, false, no, off.`,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const encryptRaw = e.MSSQL_ENCRYPT;
|
|
144
|
+
if (!isUnset(encryptRaw)) {
|
|
145
|
+
const normalized = encryptRaw.trim().toLowerCase();
|
|
146
|
+
if (normalized !== 'strict' &&
|
|
147
|
+
!TRUE_VALUES.has(normalized) &&
|
|
148
|
+
!FALSE_VALUES.has(normalized)) {
|
|
149
|
+
addIssue({
|
|
150
|
+
message: 'MSSQL_ENCRYPT must be one of: 1, true, yes, on, 0, false, no, off, strict.',
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
for (const rule of INTEGER_ENV_RULES) {
|
|
155
|
+
const raw = e[rule.key];
|
|
156
|
+
if (typeof raw !== 'string' || isUnset(raw))
|
|
157
|
+
continue;
|
|
158
|
+
const trimmed = raw.trim();
|
|
159
|
+
if (!INTEGER_PATTERN.test(trimmed)) {
|
|
160
|
+
addIssue({ message: `${rule.key} must be an integer.` });
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const value = Number.parseInt(trimmed, 10);
|
|
164
|
+
if (value < rule.min || (rule.max !== undefined && value > rule.max)) {
|
|
165
|
+
const range = rule.max === undefined
|
|
166
|
+
? `greater than or equal to ${rule.min}`
|
|
167
|
+
: `between ${rule.min} and ${rule.max}`;
|
|
168
|
+
addIssue({ message: `${rule.key} must be ${range}.` });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function rawEnvChecks(e, addIssue) {
|
|
173
|
+
const rawAuth = normalizeAuthType(e.MSSQL_AUTH_TYPE);
|
|
174
|
+
if (rawAuth !== undefined && !ALLOWED_AUTH.has(rawAuth)) {
|
|
175
|
+
addIssue({
|
|
176
|
+
message: `Unsupported MSSQL_AUTH_TYPE "${e.MSSQL_AUTH_TYPE?.trim()}". Use default (unset), ntlm, azure-active-directory-password, azure-active-directory-access-token, or azure-active-directory-service-principal-secret.`,
|
|
177
|
+
});
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const authType = inferAuthType(e);
|
|
181
|
+
const user = (e.MSSQL_USER ?? '').trim();
|
|
182
|
+
if (authType === undefined) {
|
|
183
|
+
if (user === '') {
|
|
184
|
+
addIssue({
|
|
185
|
+
message: 'MSSQL_USER is required for SQL authentication (or set MSSQL_AUTH_TYPE).',
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
if (e.MSSQL_PASSWORD === undefined) {
|
|
189
|
+
addIssue({
|
|
190
|
+
message: 'MSSQL_PASSWORD is required for SQL authentication.',
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (authType === 'ntlm') {
|
|
196
|
+
if (user === '')
|
|
197
|
+
addIssue({
|
|
198
|
+
message: 'MSSQL_USER is required when MSSQL_AUTH_TYPE=ntlm.',
|
|
199
|
+
});
|
|
200
|
+
if (e.MSSQL_PASSWORD === undefined) {
|
|
201
|
+
addIssue({
|
|
202
|
+
message: 'MSSQL_PASSWORD is required when MSSQL_AUTH_TYPE=ntlm.',
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
if (!e.MSSQL_DOMAIN?.trim())
|
|
206
|
+
addIssue({
|
|
207
|
+
message: 'MSSQL_DOMAIN is required when MSSQL_AUTH_TYPE=ntlm.',
|
|
208
|
+
});
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (authType === 'azure-active-directory-password') {
|
|
212
|
+
if (user === '') {
|
|
213
|
+
addIssue({
|
|
214
|
+
message: 'MSSQL_USER is required when MSSQL_AUTH_TYPE=azure-active-directory-password.',
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
if (e.MSSQL_PASSWORD === undefined) {
|
|
218
|
+
addIssue({
|
|
219
|
+
message: 'MSSQL_PASSWORD is required when MSSQL_AUTH_TYPE=azure-active-directory-password.',
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
if (!e.MSSQL_AZURE_CLIENT_ID?.trim()) {
|
|
223
|
+
addIssue({
|
|
224
|
+
message: 'MSSQL_AZURE_CLIENT_ID is required when MSSQL_AUTH_TYPE=azure-active-directory-password.',
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (authType === 'azure-active-directory-access-token') {
|
|
230
|
+
if (!e.MSSQL_AZURE_ACCESS_TOKEN?.trim()) {
|
|
231
|
+
addIssue({
|
|
232
|
+
message: 'MSSQL_AZURE_ACCESS_TOKEN is required when MSSQL_AUTH_TYPE=azure-active-directory-access-token.',
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (authType === 'azure-active-directory-service-principal-secret') {
|
|
238
|
+
if (!e.MSSQL_AZURE_CLIENT_ID?.trim()) {
|
|
239
|
+
addIssue({
|
|
240
|
+
message: 'MSSQL_AZURE_CLIENT_ID is required when MSSQL_AUTH_TYPE=azure-active-directory-service-principal-secret.',
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
if (!e.MSSQL_AZURE_CLIENT_SECRET?.trim()) {
|
|
244
|
+
addIssue({
|
|
245
|
+
message: 'MSSQL_AZURE_CLIENT_SECRET is required when MSSQL_AUTH_TYPE=azure-active-directory-service-principal-secret.',
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
if (!e.MSSQL_AZURE_TENANT_ID?.trim()) {
|
|
249
|
+
addIssue({
|
|
250
|
+
message: 'MSSQL_AZURE_TENANT_ID is required when MSSQL_AUTH_TYPE=azure-active-directory-service-principal-secret.',
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function rawPoolChecks(e, addIssue) {
|
|
256
|
+
const poolMaxRaw = e.MSSQL_POOL_MAX?.trim();
|
|
257
|
+
const poolMinRaw = e.MSSQL_POOL_MIN?.trim();
|
|
258
|
+
if ((poolMaxRaw !== undefined &&
|
|
259
|
+
poolMaxRaw !== '' &&
|
|
260
|
+
!INTEGER_PATTERN.test(poolMaxRaw)) ||
|
|
261
|
+
(poolMinRaw !== undefined &&
|
|
262
|
+
poolMinRaw !== '' &&
|
|
263
|
+
!INTEGER_PATTERN.test(poolMinRaw))) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const poolMax = parseIntEnv(e.MSSQL_POOL_MAX) ?? 10;
|
|
267
|
+
const poolMin = parseIntEnv(e.MSSQL_POOL_MIN) ?? 0;
|
|
268
|
+
if (poolMin > poolMax) {
|
|
269
|
+
addIssue({
|
|
270
|
+
message: `MSSQL_POOL_MIN (${poolMin}) cannot be greater than MSSQL_POOL_MAX (${poolMax}).`,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function rawTdsChecks(e, addIssue) {
|
|
275
|
+
const tdsRaw = e.MSSQL_TDS_VERSION?.trim();
|
|
276
|
+
if (tdsRaw === undefined || tdsRaw === '')
|
|
277
|
+
return;
|
|
278
|
+
if (!TDS_VERSIONS.has(tdsRaw)) {
|
|
279
|
+
addIssue({
|
|
280
|
+
message: `Invalid MSSQL_TDS_VERSION "${tdsRaw}". Use one of: ${[...TDS_VERSIONS].join(', ')}.`,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const appConfigSchema = v.pipe(envSchema, v.rawCheck(({ dataset, addIssue }) => {
|
|
285
|
+
rawScalarChecks(dataset.value, addIssue);
|
|
286
|
+
}), v.rawCheck(({ dataset, addIssue }) => {
|
|
287
|
+
rawEnvChecks(dataset.value, addIssue);
|
|
288
|
+
}), v.rawCheck(({ dataset, addIssue }) => {
|
|
289
|
+
rawPoolChecks(dataset.value, addIssue);
|
|
290
|
+
}), v.rawCheck(({ dataset, addIssue }) => {
|
|
291
|
+
rawTdsChecks(dataset.value, addIssue);
|
|
292
|
+
}), v.transform((e) => {
|
|
293
|
+
const authType = inferAuthType(e);
|
|
294
|
+
const port = parseIntEnv(e.MSSQL_PORT) ?? 1433;
|
|
295
|
+
const queryTimeoutValue = parseIntEnv(e.MSSQL_QUERY_TIMEOUT_MS);
|
|
296
|
+
const queryTimeoutMs = queryTimeoutValue === undefined || queryTimeoutValue === 0
|
|
297
|
+
? undefined
|
|
298
|
+
: queryTimeoutValue;
|
|
299
|
+
const connectionTimeoutValue = parseIntEnv(e.MSSQL_CONNECTION_TIMEOUT_MS);
|
|
300
|
+
const connectionTimeoutMs = connectionTimeoutValue === undefined || connectionTimeoutValue === 0
|
|
301
|
+
? undefined
|
|
302
|
+
: connectionTimeoutValue;
|
|
303
|
+
const ca = readPemFileOptional('MSSQL_TLS_CA_FILE', e.MSSQL_TLS_CA_FILE);
|
|
304
|
+
const cert = readPemFileOptional('MSSQL_TLS_CERT_FILE', e.MSSQL_TLS_CERT_FILE);
|
|
305
|
+
const key = readPemFileOptional('MSSQL_TLS_KEY_FILE', e.MSSQL_TLS_KEY_FILE);
|
|
306
|
+
const passphrase = e.MSSQL_TLS_KEY_PASSPHRASE?.trim() || undefined;
|
|
307
|
+
let cryptoCredentialsDetails;
|
|
308
|
+
if (ca !== undefined ||
|
|
309
|
+
cert !== undefined ||
|
|
310
|
+
key !== undefined ||
|
|
311
|
+
passphrase !== undefined) {
|
|
312
|
+
cryptoCredentialsDetails = {};
|
|
313
|
+
if (ca !== undefined)
|
|
314
|
+
cryptoCredentialsDetails.ca = ca;
|
|
315
|
+
if (cert !== undefined)
|
|
316
|
+
cryptoCredentialsDetails.cert = cert;
|
|
317
|
+
if (key !== undefined)
|
|
318
|
+
cryptoCredentialsDetails.key = key;
|
|
319
|
+
if (passphrase !== undefined)
|
|
320
|
+
cryptoCredentialsDetails.passphrase = passphrase;
|
|
321
|
+
}
|
|
322
|
+
const retriesValue = parseIntEnv(e.MSSQL_MAX_RETRIES_ON_TRANSIENT_ERRORS);
|
|
323
|
+
const maxRetriesOnTransientErrors = retriesValue === undefined || retriesValue === 0
|
|
324
|
+
? undefined
|
|
325
|
+
: retriesValue;
|
|
326
|
+
const retryIntervalValue = parseIntEnv(e.MSSQL_CONNECTION_RETRY_INTERVAL_MS);
|
|
327
|
+
const connectionRetryIntervalMs = retryIntervalValue === undefined || retryIntervalValue === 0
|
|
328
|
+
? undefined
|
|
329
|
+
: retryIntervalValue;
|
|
330
|
+
const tdsRaw = e.MSSQL_TDS_VERSION?.trim();
|
|
331
|
+
const tdsVersion = tdsRaw === '' || tdsRaw === undefined ? undefined : tdsRaw;
|
|
332
|
+
const instanceTrim = e.MSSQL_INSTANCE_NAME?.trim();
|
|
333
|
+
const instanceName = instanceTrim === '' ? undefined : instanceTrim;
|
|
334
|
+
const multiRaw = e.MSSQL_MULTI_SUBNET_FAILOVER;
|
|
335
|
+
const multiSubnetFailover = isUnset(multiRaw)
|
|
336
|
+
? undefined
|
|
337
|
+
: parseBool(multiRaw, false);
|
|
338
|
+
const readOnlyRaw = e.MSSQL_READ_ONLY_INTENT;
|
|
339
|
+
const readOnlyIntent = isUnset(readOnlyRaw)
|
|
340
|
+
? undefined
|
|
341
|
+
: parseBool(readOnlyRaw, false);
|
|
342
|
+
const useUtcRaw = e.MSSQL_USE_UTC;
|
|
343
|
+
const useUtc = isUnset(useUtcRaw) ? undefined : parseBool(useUtcRaw, true);
|
|
344
|
+
const poolMax = parseIntEnv(e.MSSQL_POOL_MAX) ?? 10;
|
|
345
|
+
const poolMin = parseIntEnv(e.MSSQL_POOL_MIN) ?? 0;
|
|
346
|
+
const poolIdleTimeoutMs = parseIntEnv(e.MSSQL_POOL_IDLE_TIMEOUT_MS) ?? 30_000;
|
|
347
|
+
const appTrim = e.MSSQL_APP_NAME?.trim();
|
|
348
|
+
const appName = appTrim === '' ? undefined : appTrim;
|
|
349
|
+
const tlsServerTrim = e.MSSQL_TLS_SERVER_NAME?.trim();
|
|
350
|
+
return {
|
|
351
|
+
server: e.MSSQL_SERVER.trim(),
|
|
352
|
+
port,
|
|
353
|
+
user: (e.MSSQL_USER ?? '').trim(),
|
|
354
|
+
password: e.MSSQL_PASSWORD ?? '',
|
|
355
|
+
database: e.MSSQL_DATABASE.trim(),
|
|
356
|
+
encrypt: parseEncrypt(e.MSSQL_ENCRYPT),
|
|
357
|
+
trustServerCertificate: parseBool(e.MSSQL_TRUST_SERVER_CERTIFICATE, false),
|
|
358
|
+
allowWrites: parseBool(e.MSSQL_ALLOW_WRITES, false),
|
|
359
|
+
queryTimeoutMs,
|
|
360
|
+
authType,
|
|
361
|
+
domain: e.MSSQL_DOMAIN?.trim() || undefined,
|
|
362
|
+
tlsServerName: tlsServerTrim === '' ? undefined : tlsServerTrim,
|
|
363
|
+
cryptoCredentialsDetails,
|
|
364
|
+
connectionTimeoutMs,
|
|
365
|
+
instanceName,
|
|
366
|
+
multiSubnetFailover,
|
|
367
|
+
readOnlyIntent,
|
|
368
|
+
maxRetriesOnTransientErrors,
|
|
369
|
+
connectionRetryIntervalMs,
|
|
370
|
+
poolMax,
|
|
371
|
+
poolMin,
|
|
372
|
+
poolIdleTimeoutMs,
|
|
373
|
+
appName,
|
|
374
|
+
useUtc,
|
|
375
|
+
tdsVersion,
|
|
376
|
+
azureClientId: e.MSSQL_AZURE_CLIENT_ID?.trim() || undefined,
|
|
377
|
+
azureTenantId: e.MSSQL_AZURE_TENANT_ID?.trim() || undefined,
|
|
378
|
+
azureClientSecret: e.MSSQL_AZURE_CLIENT_SECRET ?? undefined,
|
|
379
|
+
azureAccessToken: e.MSSQL_AZURE_ACCESS_TOKEN?.trim() || undefined,
|
|
380
|
+
};
|
|
381
|
+
}));
|
|
382
|
+
function readProcessEnv() {
|
|
383
|
+
return {
|
|
27
384
|
MSSQL_SERVER: process.env.MSSQL_SERVER,
|
|
28
385
|
MSSQL_USER: process.env.MSSQL_USER,
|
|
29
386
|
MSSQL_PASSWORD: process.env.MSSQL_PASSWORD,
|
|
@@ -32,43 +389,128 @@ export function loadConfig() {
|
|
|
32
389
|
MSSQL_ENCRYPT: process.env.MSSQL_ENCRYPT,
|
|
33
390
|
MSSQL_TRUST_SERVER_CERTIFICATE: process.env.MSSQL_TRUST_SERVER_CERTIFICATE,
|
|
34
391
|
MSSQL_ALLOW_WRITES: process.env.MSSQL_ALLOW_WRITES,
|
|
35
|
-
MSSQL_MAX_ROWS: process.env.MSSQL_MAX_ROWS,
|
|
36
392
|
MSSQL_QUERY_TIMEOUT_MS: process.env.MSSQL_QUERY_TIMEOUT_MS,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
393
|
+
MSSQL_TLS_SERVER_NAME: process.env.MSSQL_TLS_SERVER_NAME,
|
|
394
|
+
MSSQL_TLS_CA_FILE: process.env.MSSQL_TLS_CA_FILE,
|
|
395
|
+
MSSQL_TLS_CERT_FILE: process.env.MSSQL_TLS_CERT_FILE,
|
|
396
|
+
MSSQL_TLS_KEY_FILE: process.env.MSSQL_TLS_KEY_FILE,
|
|
397
|
+
MSSQL_TLS_KEY_PASSPHRASE: process.env.MSSQL_TLS_KEY_PASSPHRASE,
|
|
398
|
+
MSSQL_CONNECTION_TIMEOUT_MS: process.env.MSSQL_CONNECTION_TIMEOUT_MS,
|
|
399
|
+
MSSQL_DOMAIN: process.env.MSSQL_DOMAIN,
|
|
400
|
+
MSSQL_INSTANCE_NAME: process.env.MSSQL_INSTANCE_NAME,
|
|
401
|
+
MSSQL_MULTI_SUBNET_FAILOVER: process.env.MSSQL_MULTI_SUBNET_FAILOVER,
|
|
402
|
+
MSSQL_READ_ONLY_INTENT: process.env.MSSQL_READ_ONLY_INTENT,
|
|
403
|
+
MSSQL_MAX_RETRIES_ON_TRANSIENT_ERRORS: process.env.MSSQL_MAX_RETRIES_ON_TRANSIENT_ERRORS,
|
|
404
|
+
MSSQL_CONNECTION_RETRY_INTERVAL_MS: process.env.MSSQL_CONNECTION_RETRY_INTERVAL_MS,
|
|
405
|
+
MSSQL_POOL_MAX: process.env.MSSQL_POOL_MAX,
|
|
406
|
+
MSSQL_POOL_MIN: process.env.MSSQL_POOL_MIN,
|
|
407
|
+
MSSQL_POOL_IDLE_TIMEOUT_MS: process.env.MSSQL_POOL_IDLE_TIMEOUT_MS,
|
|
408
|
+
MSSQL_APP_NAME: process.env.MSSQL_APP_NAME,
|
|
409
|
+
MSSQL_USE_UTC: process.env.MSSQL_USE_UTC,
|
|
410
|
+
MSSQL_TDS_VERSION: process.env.MSSQL_TDS_VERSION,
|
|
411
|
+
MSSQL_AUTH_TYPE: process.env.MSSQL_AUTH_TYPE,
|
|
412
|
+
MSSQL_AZURE_CLIENT_ID: process.env.MSSQL_AZURE_CLIENT_ID,
|
|
413
|
+
MSSQL_AZURE_TENANT_ID: process.env.MSSQL_AZURE_TENANT_ID,
|
|
414
|
+
MSSQL_AZURE_CLIENT_SECRET: process.env.MSSQL_AZURE_CLIENT_SECRET,
|
|
415
|
+
MSSQL_AZURE_ACCESS_TOKEN: process.env.MSSQL_AZURE_ACCESS_TOKEN,
|
|
58
416
|
};
|
|
59
417
|
}
|
|
418
|
+
export function loadConfig() {
|
|
419
|
+
return v.parse(appConfigSchema, readProcessEnv());
|
|
420
|
+
}
|
|
60
421
|
export function mssqlDriverConfig(cfg) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
422
|
+
const options = {
|
|
423
|
+
encrypt: cfg.encrypt,
|
|
424
|
+
trustServerCertificate: cfg.trustServerCertificate,
|
|
425
|
+
};
|
|
426
|
+
if (cfg.tlsServerName !== undefined)
|
|
427
|
+
options.serverName = cfg.tlsServerName;
|
|
428
|
+
if (cfg.instanceName !== undefined)
|
|
429
|
+
options.instanceName = cfg.instanceName;
|
|
430
|
+
if (cfg.multiSubnetFailover !== undefined)
|
|
431
|
+
options.multiSubnetFailover = cfg.multiSubnetFailover;
|
|
432
|
+
if (cfg.readOnlyIntent !== undefined)
|
|
433
|
+
options.readOnlyIntent = cfg.readOnlyIntent;
|
|
434
|
+
if (cfg.maxRetriesOnTransientErrors !== undefined) {
|
|
435
|
+
options.maxRetriesOnTransientErrors = cfg.maxRetriesOnTransientErrors;
|
|
436
|
+
}
|
|
437
|
+
if (cfg.connectionRetryIntervalMs !== undefined) {
|
|
438
|
+
options.connectionRetryInterval = cfg.connectionRetryIntervalMs;
|
|
439
|
+
}
|
|
440
|
+
if (cfg.cryptoCredentialsDetails !== undefined) {
|
|
441
|
+
options.cryptoCredentialsDetails = cfg.cryptoCredentialsDetails;
|
|
442
|
+
}
|
|
443
|
+
if (cfg.appName !== undefined)
|
|
444
|
+
options.appName = cfg.appName;
|
|
445
|
+
if (cfg.useUtc !== undefined)
|
|
446
|
+
options.useUTC = cfg.useUtc;
|
|
447
|
+
if (cfg.tdsVersion !== undefined) {
|
|
448
|
+
options.tdsVersion = cfg.tdsVersion;
|
|
449
|
+
}
|
|
450
|
+
let authentication;
|
|
451
|
+
if (cfg.authType === 'ntlm') {
|
|
452
|
+
authentication = {
|
|
453
|
+
type: 'ntlm',
|
|
454
|
+
options: {
|
|
455
|
+
userName: cfg.user,
|
|
456
|
+
password: cfg.password,
|
|
457
|
+
domain: cfg.domain,
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
else if (cfg.authType === 'azure-active-directory-password') {
|
|
462
|
+
authentication = {
|
|
463
|
+
type: 'azure-active-directory-password',
|
|
464
|
+
options: {
|
|
465
|
+
userName: cfg.user,
|
|
466
|
+
password: cfg.password,
|
|
467
|
+
clientId: cfg.azureClientId,
|
|
468
|
+
tenantId: cfg.azureTenantId ?? 'common',
|
|
469
|
+
},
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
else if (cfg.authType === 'azure-active-directory-access-token') {
|
|
473
|
+
authentication = {
|
|
474
|
+
type: 'azure-active-directory-access-token',
|
|
475
|
+
options: { token: cfg.azureAccessToken },
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
else if (cfg.authType === 'azure-active-directory-service-principal-secret') {
|
|
479
|
+
authentication = {
|
|
480
|
+
type: 'azure-active-directory-service-principal-secret',
|
|
481
|
+
options: {
|
|
482
|
+
clientId: cfg.azureClientId,
|
|
483
|
+
clientSecret: cfg.azureClientSecret,
|
|
484
|
+
tenantId: cfg.azureTenantId,
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
const base = {
|
|
64
489
|
server: cfg.server,
|
|
65
|
-
port: cfg.port,
|
|
66
490
|
database: cfg.database,
|
|
67
|
-
pool: {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
491
|
+
pool: {
|
|
492
|
+
max: cfg.poolMax,
|
|
493
|
+
min: cfg.poolMin,
|
|
494
|
+
idleTimeoutMillis: cfg.poolIdleTimeoutMs,
|
|
71
495
|
},
|
|
496
|
+
options,
|
|
72
497
|
requestTimeout: cfg.queryTimeoutMs,
|
|
73
498
|
};
|
|
499
|
+
if (cfg.connectionTimeoutMs !== undefined) {
|
|
500
|
+
base.connectionTimeout = cfg.connectionTimeoutMs;
|
|
501
|
+
}
|
|
502
|
+
if (cfg.instanceName === undefined) {
|
|
503
|
+
base.port = cfg.port;
|
|
504
|
+
}
|
|
505
|
+
if (authentication !== undefined) {
|
|
506
|
+
base.authentication = authentication;
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
base.user = cfg.user;
|
|
510
|
+
base.password = cfg.password;
|
|
511
|
+
if (cfg.domain !== undefined) {
|
|
512
|
+
base.domain = cfg.domain;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return base;
|
|
74
516
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import sql from 'mssql';
|
|
3
|
-
import * as v from 'valibot';
|
|
4
2
|
import { Server } from '@modelcontextprotocol/sdk/server';
|
|
5
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
4
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import sql from 'mssql';
|
|
6
|
+
import * as v from 'valibot';
|
|
7
7
|
import { loadConfig, mssqlDriverConfig } from './config.js';
|
|
8
|
-
import { assertReadOnlySql,
|
|
8
|
+
import { assertReadOnlySql, ReadOnlySqlError } from './readonly-sql.js';
|
|
9
9
|
import { mssqlDescribeTableSchema, mssqlListTablesSchema, mssqlQuerySchema, valibotToJsonSchema, } from './schemas.js';
|
|
10
10
|
const SHARED_TOOL_PREAMBLE = 'Connection uses MSSQL_* environment variables on the MCP server process. Arbitrary SQL is dangerous; prefer a read-only or narrowly scoped database user.';
|
|
11
11
|
function toolError(message) {
|
|
@@ -19,17 +19,32 @@ function jsonResult(data) {
|
|
|
19
19
|
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
|
+
function logToolFailure(toolName, err) {
|
|
23
|
+
console.error(`[${toolName}]`, err);
|
|
24
|
+
}
|
|
25
|
+
function toolExecutionError(toolName, err) {
|
|
26
|
+
if (err instanceof ReadOnlySqlError) {
|
|
27
|
+
return toolError(err.message);
|
|
28
|
+
}
|
|
29
|
+
logToolFailure(toolName, err);
|
|
30
|
+
switch (toolName) {
|
|
31
|
+
case 'mssql_query':
|
|
32
|
+
return toolError('Query failed. Check SQL syntax, permissions, timeout settings, and database connectivity.');
|
|
33
|
+
case 'mssql_list_tables':
|
|
34
|
+
return toolError('Failed to list tables. Check permissions and database connectivity.');
|
|
35
|
+
case 'mssql_describe_table':
|
|
36
|
+
return toolError('Failed to describe the table. Check the schema/table name, permissions, and connectivity.');
|
|
37
|
+
default:
|
|
38
|
+
return toolError('The request failed. Check the server logs for details.');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
22
41
|
async function runQuery(pool, cfg, batch) {
|
|
23
42
|
assertReadOnlySql(batch, cfg.allowWrites);
|
|
24
|
-
const
|
|
25
|
-
const result = await pool.request().query(wrapped);
|
|
43
|
+
const result = await pool.request().query(batch);
|
|
26
44
|
const recordsets = result.recordsets;
|
|
27
45
|
return jsonResult({
|
|
28
46
|
rowsAffected: result.rowsAffected,
|
|
29
47
|
recordsets,
|
|
30
|
-
rowCountNote: cfg.maxRows !== undefined
|
|
31
|
-
? `ROWCOUNT capped at ${cfg.maxRows} per batch (SET ROWCOUNT).`
|
|
32
|
-
: undefined,
|
|
33
48
|
});
|
|
34
49
|
}
|
|
35
50
|
async function runListTables(pool, schemaFilter) {
|
|
@@ -114,14 +129,13 @@ async function main() {
|
|
|
114
129
|
if (v.isValiError(err)) {
|
|
115
130
|
return toolError(`Invalid arguments: ${err.message}`);
|
|
116
131
|
}
|
|
117
|
-
|
|
118
|
-
return toolError(msg);
|
|
132
|
+
return toolExecutionError(name, err);
|
|
119
133
|
}
|
|
120
134
|
});
|
|
121
135
|
const transport = new StdioServerTransport();
|
|
122
136
|
await server.connect(transport);
|
|
123
137
|
}
|
|
124
138
|
main().catch((err) => {
|
|
125
|
-
console.error(err);
|
|
139
|
+
console.error('Fatal error in main():', err);
|
|
126
140
|
process.exit(1);
|
|
127
141
|
});
|
package/dist/readonly-sql.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Best-effort read-only gate. Not a substitute for database permissions.
|
|
3
3
|
*/
|
|
4
|
+
export declare class ReadOnlySqlError extends Error {
|
|
5
|
+
constructor(message: string);
|
|
6
|
+
}
|
|
4
7
|
export declare function stripSqlComments(text: string): string;
|
|
5
8
|
/** Masks single-quoted and N'...' string literals so keywords inside literals are ignored. */
|
|
6
9
|
export declare function maskSqlStringLiterals(text: string): string;
|
|
7
10
|
export declare function assertReadOnlySql(sql: string, allowWrites: boolean): void;
|
|
8
|
-
export declare function wrapWithRowCount(sql: string, maxRows: number | undefined): string;
|
package/dist/readonly-sql.js
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
* Best-effort read-only gate. Not a substitute for database permissions.
|
|
3
3
|
*/
|
|
4
4
|
const FORBIDDEN = /\b(INSERT|UPDATE|DELETE|MERGE|DROP|ALTER|CREATE|TRUNCATE|EXEC|EXECUTE|GRANT|REVOKE|DENY|BULK)\b/i;
|
|
5
|
+
export class ReadOnlySqlError extends Error {
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'ReadOnlySqlError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
5
11
|
export function stripSqlComments(text) {
|
|
6
12
|
let s = text.replace(/\/\*[\s\S]*?\*\//g, ' ');
|
|
7
13
|
s = s.replace(/--[^\n\r]*/g, ' ');
|
|
@@ -55,12 +61,6 @@ export function assertReadOnlySql(sql, allowWrites) {
|
|
|
55
61
|
return;
|
|
56
62
|
const probe = maskSqlStringLiterals(stripSqlComments(sql));
|
|
57
63
|
if (FORBIDDEN.test(probe)) {
|
|
58
|
-
throw new
|
|
64
|
+
throw new ReadOnlySqlError('Read-only mode: this batch contains a blocked keyword (INSERT, UPDATE, DELETE, MERGE, DDL, EXEC, etc.). Set MSSQL_ALLOW_WRITES=true to allow writes (still use a least-privilege login).');
|
|
59
65
|
}
|
|
60
66
|
}
|
|
61
|
-
export function wrapWithRowCount(sql, maxRows) {
|
|
62
|
-
if (maxRows === undefined || maxRows <= 0)
|
|
63
|
-
return sql;
|
|
64
|
-
const n = Math.floor(maxRows);
|
|
65
|
-
return `SET ROWCOUNT ${n}; ${sql}; SET ROWCOUNT 0;`;
|
|
66
|
-
}
|
package/dist/schemas.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import * as v from 'valibot';
|
|
2
1
|
import { toJsonSchema } from '@valibot/to-json-schema';
|
|
2
|
+
import * as v from 'valibot';
|
|
3
3
|
/** MCP tool inputSchema: JSON Schema derived from Valibot (descriptions flow to clients). */
|
|
4
4
|
export function valibotToJsonSchema(schema) {
|
|
5
5
|
const js = toJsonSchema(schema);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adriancy/mcp-mssql",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"valibot": "^1.1.0"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
|
+
"@biomejs/biome": "^2.4.10",
|
|
22
23
|
"@types/mssql": "^9.1.8",
|
|
23
24
|
"@types/node": "^22.10.0",
|
|
24
25
|
"tsx": "^4.19.2",
|
|
@@ -26,6 +27,10 @@
|
|
|
26
27
|
},
|
|
27
28
|
"scripts": {
|
|
28
29
|
"build": "tsc",
|
|
30
|
+
"lint": "biome lint .",
|
|
31
|
+
"format": "biome format --write .",
|
|
32
|
+
"check": "biome check . --write",
|
|
33
|
+
"test": "node --import tsx --test",
|
|
29
34
|
"start": "node dist/index.js",
|
|
30
35
|
"dev": "node --import tsx src/index.ts"
|
|
31
36
|
}
|