@adriancy/mcp-mssql 1.0.0 → 1.0.2

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 CHANGED
@@ -1,21 +1,52 @@
1
1
  # Required
2
2
  MSSQL_SERVER=localhost
3
- MSSQL_USER=sa
4
- MSSQL_PASSWORD=
5
- MSSQL_DATABASE=master
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
+ # Hostname on the SQL Server TLS cert when MSSQL_SERVER is an IP (Node.js cannot use an IP as TLS SNI).
14
+ # MSSQL_TLS_SERVER_NAME=
15
+ # Paths on the MCP host; PEM contents read at startup (fail fast if missing).
16
+ # MSSQL_TLS_CA_FILE=
17
+ # MSSQL_TLS_CERT_FILE=
18
+ # MSSQL_TLS_KEY_FILE=
19
+ # MSSQL_TLS_KEY_PASSPHRASE=
20
+
21
+ # Connectivity / resilience
22
+ # MSSQL_CONNECTION_TIMEOUT_MS=
23
+ # MSSQL_DOMAIN=
24
+ # MSSQL_INSTANCE_NAME=
25
+ # MSSQL_MULTI_SUBNET_FAILOVER=false
26
+ # MSSQL_READ_ONLY_INTENT=false
27
+ # MSSQL_MAX_RETRIES_ON_TRANSIENT_ERRORS=
28
+ # MSSQL_CONNECTION_RETRY_INTERVAL_MS=
29
+
30
+ # Pool (defaults: max 10, min 0, idle 30000 ms)
31
+ # MSSQL_POOL_MAX=10
32
+ # MSSQL_POOL_MIN=0
33
+ # MSSQL_POOL_IDLE_TIMEOUT_MS=30000
34
+
35
+ # Session / protocol
36
+ # MSSQL_APP_NAME=
37
+ # MSSQL_USE_UTC=true
38
+ # MSSQL_TDS_VERSION=
39
+
40
+ # Authentication (unset = SQL login via MSSQL_USER / MSSQL_PASSWORD).
41
+ # ntlm | azure-active-directory-password | azure-active-directory-access-token | azure-active-directory-service-principal-secret
42
+ # MSSQL_AUTH_TYPE=
43
+ # MSSQL_AZURE_CLIENT_ID=
44
+ # MSSQL_AZURE_TENANT_ID=
45
+ # MSSQL_AZURE_CLIENT_SECRET=
46
+ # MSSQL_AZURE_ACCESS_TOKEN=
13
47
 
14
48
  # Safety: when false/ unset, blocks common write/DDL/exec patterns (heuristic only)
15
49
  # MSSQL_ALLOW_WRITES=false
16
50
 
17
- # Cap rows returned (SET ROWCOUNT); unset = no cap
18
- # MSSQL_MAX_ROWS=5000
19
-
20
51
  # Request timeout in ms (driver)
21
52
  # MSSQL_QUERY_TIMEOUT_MS=30000
package/README.md CHANGED
@@ -1,49 +1,20 @@
1
- # mssql-mcp
1
+ # `@adriancy/mcp-mssql`
2
2
 
3
- MCP server (stdio) that connects to Microsoft SQL Server using the `mssql` driver. Tool arguments are defined and validated with [Valibot](https://valibot.dev); JSON Schema for clients is generated with `@valibot/to-json-schema`.
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
- **Implementation note:** This project uses the low-level `@modelcontextprotocol/sdk` `Server` class rather than `McpServer`, because the current SDK’s `McpServer.registerTool` path is built around Zod for schema export and validation. Tools are registered with `ListTools` / `tools/call` handlers and Valibot parsing inside the handlers.
5
+ ## Use it in Cursor
6
6
 
7
- ## Tools
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
- ## Cursor MCP configuration
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-mcp"],
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
- **Local checkout:** use the absolute path to `dist/index.js`:
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": ["/home/adrian/code/mcp/mssql-mcp/dist/index.js"],
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 paths. Do **not** use **`pnpm`** as `command` for MCP: if pnpm prints errors to stdout, Cursor can show JSON parse errors because the protocol uses stdout. **`npx`** is fine for the published package: it execs the `mssql-mcp` bin (Node) after install.
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 | — | TLS SNI / cert hostname when `MSSQL_SERVER` is an IP or differs from the cert (required for IP + encrypted connections; must be a hostname, not an 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
- ### Dev mode without a build (still use `node`)
98
+ ## Tools
82
99
 
83
- Set **`cwd`** to this repo so `node` can resolve `tsx` from `node_modules`:
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
- ```json
86
- {
87
- "mcpServers": {
88
- "mssql": {
89
- "command": "node",
90
- "args": ["--import", "tsx", "/home/adrian/code/mcp/src/index.ts"],
91
- "cwd": "/home/adrian/code/mcp",
92
- "env": { }
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
- Fill `env` the same as in the example above. Run `pnpm install` locally first so `tsx` exists.
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,408 @@
1
+ import * as fs from 'node:fs';
2
+ import { isIP } from 'node:net';
3
+ import * as path from 'node:path';
1
4
  import * as v from 'valibot';
5
+ const TRUE_VALUES = new Set(['1', 'true', 'yes', 'on']);
6
+ const FALSE_VALUES = new Set(['0', 'false', 'no', 'off']);
7
+ const INTEGER_PATTERN = /^-?\d+$/;
8
+ function isUnset(raw) {
9
+ return raw === undefined || raw.trim() === '';
10
+ }
2
11
  function parseBool(raw, defaultValue) {
3
- if (raw === undefined || raw === '')
12
+ const trimmed = raw?.trim();
13
+ if (trimmed === undefined || trimmed === '')
4
14
  return defaultValue;
5
- return /^(1|true|yes|on)$/i.test(raw);
15
+ const normalized = trimmed.toLowerCase();
16
+ if (TRUE_VALUES.has(normalized))
17
+ return true;
18
+ if (FALSE_VALUES.has(normalized))
19
+ return false;
20
+ throw new Error(`Invalid boolean value "${raw}".`);
6
21
  }
7
- function parseIntEnv(raw, defaultValue) {
8
- if (raw === undefined || raw === '')
9
- return defaultValue;
10
- const n = Number.parseInt(raw, 10);
11
- return Number.isFinite(n) ? n : defaultValue;
22
+ function parseIntEnv(raw) {
23
+ const trimmed = raw?.trim();
24
+ if (trimmed === undefined || trimmed === '')
25
+ return undefined;
26
+ if (!INTEGER_PATTERN.test(trimmed)) {
27
+ throw new Error(`Invalid integer value "${raw}".`);
28
+ }
29
+ return Number.parseInt(trimmed, 10);
30
+ }
31
+ function parseEncrypt(raw) {
32
+ const trimmed = raw?.trim();
33
+ if (trimmed === undefined || trimmed === '')
34
+ return true;
35
+ const t = trimmed.toLowerCase();
36
+ if (t === 'strict')
37
+ return 'strict';
38
+ return parseBool(raw, true);
39
+ }
40
+ /** True when MSSQL_ENCRYPT explicitly disables TLS (default is encrypt on). */
41
+ function isEncryptExplicitlyDisabled(raw) {
42
+ const trimmed = raw?.trim();
43
+ if (trimmed === undefined || trimmed === '')
44
+ return false;
45
+ return FALSE_VALUES.has(trimmed.toLowerCase());
46
+ }
47
+ function normalizeAuthType(raw) {
48
+ const trimmed = raw?.trim();
49
+ if (trimmed === undefined || trimmed === '')
50
+ return undefined;
51
+ const t = trimmed.toLowerCase();
52
+ if (t === '' || t === 'default')
53
+ return undefined;
54
+ return t;
55
+ }
56
+ const TDS_VERSIONS = new Set(['7_1', '7_2', '7_3_A', '7_3_B', '7_4']);
57
+ const ALLOWED_AUTH = new Set([
58
+ 'ntlm',
59
+ 'azure-active-directory-password',
60
+ 'azure-active-directory-access-token',
61
+ 'azure-active-directory-service-principal-secret',
62
+ ]);
63
+ function readPemFileOptional(label, filePath) {
64
+ const trimmed = filePath?.trim();
65
+ if (trimmed === undefined || trimmed === '')
66
+ return undefined;
67
+ const resolved = path.resolve(trimmed);
68
+ try {
69
+ return fs.readFileSync(resolved);
70
+ }
71
+ catch (err) {
72
+ const msg = err instanceof Error ? err.message : String(err);
73
+ throw new Error(`Failed to read ${label} file at ${resolved}: ${msg}`);
74
+ }
12
75
  }
13
76
  const envSchema = v.object({
14
77
  MSSQL_SERVER: v.pipe(v.string(), v.nonEmpty()),
15
- MSSQL_USER: v.pipe(v.string(), v.nonEmpty()),
16
- MSSQL_PASSWORD: v.string(),
78
+ MSSQL_USER: v.optional(v.string()),
79
+ MSSQL_PASSWORD: v.optional(v.string()),
17
80
  MSSQL_DATABASE: v.pipe(v.string(), v.nonEmpty()),
18
81
  MSSQL_PORT: v.optional(v.string()),
19
82
  MSSQL_ENCRYPT: v.optional(v.string()),
20
83
  MSSQL_TRUST_SERVER_CERTIFICATE: v.optional(v.string()),
21
84
  MSSQL_ALLOW_WRITES: v.optional(v.string()),
22
- MSSQL_MAX_ROWS: v.optional(v.string()),
23
85
  MSSQL_QUERY_TIMEOUT_MS: v.optional(v.string()),
86
+ MSSQL_TLS_SERVER_NAME: v.optional(v.string()),
87
+ MSSQL_TLS_CA_FILE: v.optional(v.string()),
88
+ MSSQL_TLS_CERT_FILE: v.optional(v.string()),
89
+ MSSQL_TLS_KEY_FILE: v.optional(v.string()),
90
+ MSSQL_TLS_KEY_PASSPHRASE: v.optional(v.string()),
91
+ MSSQL_CONNECTION_TIMEOUT_MS: v.optional(v.string()),
92
+ MSSQL_DOMAIN: v.optional(v.string()),
93
+ MSSQL_INSTANCE_NAME: v.optional(v.string()),
94
+ MSSQL_MULTI_SUBNET_FAILOVER: v.optional(v.string()),
95
+ MSSQL_READ_ONLY_INTENT: v.optional(v.string()),
96
+ MSSQL_MAX_RETRIES_ON_TRANSIENT_ERRORS: v.optional(v.string()),
97
+ MSSQL_CONNECTION_RETRY_INTERVAL_MS: v.optional(v.string()),
98
+ MSSQL_POOL_MAX: v.optional(v.string()),
99
+ MSSQL_POOL_MIN: v.optional(v.string()),
100
+ MSSQL_POOL_IDLE_TIMEOUT_MS: v.optional(v.string()),
101
+ MSSQL_APP_NAME: v.optional(v.string()),
102
+ MSSQL_USE_UTC: v.optional(v.string()),
103
+ MSSQL_TDS_VERSION: v.optional(v.string()),
104
+ MSSQL_AUTH_TYPE: v.optional(v.string()),
105
+ MSSQL_AZURE_CLIENT_ID: v.optional(v.string()),
106
+ MSSQL_AZURE_TENANT_ID: v.optional(v.string()),
107
+ MSSQL_AZURE_CLIENT_SECRET: v.optional(v.string()),
108
+ MSSQL_AZURE_ACCESS_TOKEN: v.optional(v.string()),
24
109
  });
25
- export function loadConfig() {
26
- const e = v.parse(envSchema, {
110
+ function inferAuthType(e) {
111
+ const t = normalizeAuthType(e.MSSQL_AUTH_TYPE);
112
+ if (t === undefined)
113
+ return undefined;
114
+ return t;
115
+ }
116
+ const BOOLEAN_ENV_KEYS = [
117
+ 'MSSQL_TRUST_SERVER_CERTIFICATE',
118
+ 'MSSQL_ALLOW_WRITES',
119
+ 'MSSQL_MULTI_SUBNET_FAILOVER',
120
+ 'MSSQL_READ_ONLY_INTENT',
121
+ 'MSSQL_USE_UTC',
122
+ ];
123
+ const INTEGER_ENV_RULES = [
124
+ { key: 'MSSQL_PORT', min: 1, max: 65_535 },
125
+ { key: 'MSSQL_QUERY_TIMEOUT_MS', min: 0 },
126
+ { key: 'MSSQL_CONNECTION_TIMEOUT_MS', min: 0 },
127
+ { key: 'MSSQL_MAX_RETRIES_ON_TRANSIENT_ERRORS', min: 0 },
128
+ { key: 'MSSQL_CONNECTION_RETRY_INTERVAL_MS', min: 0 },
129
+ { key: 'MSSQL_POOL_MAX', min: 1 },
130
+ { key: 'MSSQL_POOL_MIN', min: 0 },
131
+ { key: 'MSSQL_POOL_IDLE_TIMEOUT_MS', min: 0 },
132
+ ];
133
+ function rawScalarChecks(e, addIssue) {
134
+ if (e.MSSQL_SERVER.trim() === '') {
135
+ addIssue({ message: 'MSSQL_SERVER is required and cannot be blank.' });
136
+ }
137
+ if (e.MSSQL_DATABASE.trim() === '') {
138
+ addIssue({ message: 'MSSQL_DATABASE is required and cannot be blank.' });
139
+ }
140
+ for (const key of BOOLEAN_ENV_KEYS) {
141
+ const raw = e[key];
142
+ if (isUnset(raw))
143
+ continue;
144
+ const normalized = raw.trim().toLowerCase();
145
+ if (!TRUE_VALUES.has(normalized) && !FALSE_VALUES.has(normalized)) {
146
+ addIssue({
147
+ message: `${key} must be one of: 1, true, yes, on, 0, false, no, off.`,
148
+ });
149
+ }
150
+ }
151
+ const encryptRaw = e.MSSQL_ENCRYPT;
152
+ if (!isUnset(encryptRaw)) {
153
+ const normalized = encryptRaw.trim().toLowerCase();
154
+ if (normalized !== 'strict' &&
155
+ !TRUE_VALUES.has(normalized) &&
156
+ !FALSE_VALUES.has(normalized)) {
157
+ addIssue({
158
+ message: 'MSSQL_ENCRYPT must be one of: 1, true, yes, on, 0, false, no, off, strict.',
159
+ });
160
+ }
161
+ }
162
+ for (const rule of INTEGER_ENV_RULES) {
163
+ const raw = e[rule.key];
164
+ if (typeof raw !== 'string' || isUnset(raw))
165
+ continue;
166
+ const trimmed = raw.trim();
167
+ if (!INTEGER_PATTERN.test(trimmed)) {
168
+ addIssue({ message: `${rule.key} must be an integer.` });
169
+ continue;
170
+ }
171
+ const value = Number.parseInt(trimmed, 10);
172
+ if (value < rule.min || (rule.max !== undefined && value > rule.max)) {
173
+ const range = rule.max === undefined
174
+ ? `greater than or equal to ${rule.min}`
175
+ : `between ${rule.min} and ${rule.max}`;
176
+ addIssue({ message: `${rule.key} must be ${range}.` });
177
+ }
178
+ }
179
+ const serverHost = e.MSSQL_SERVER.trim();
180
+ if (isIP(serverHost) !== 0 && !isEncryptExplicitlyDisabled(e.MSSQL_ENCRYPT)) {
181
+ const tlsName = e.MSSQL_TLS_SERVER_NAME?.trim();
182
+ if (tlsName === undefined || tlsName === '') {
183
+ addIssue({
184
+ message: 'MSSQL_SERVER is an IP address; Node.js TLS cannot set SNI server name to an IP. Set MSSQL_TLS_SERVER_NAME to the hostname on the server certificate (often the machine FQDN), or put that hostname in MSSQL_SERVER if DNS resolves, or set MSSQL_ENCRYPT=false if plaintext is acceptable.',
185
+ });
186
+ }
187
+ else if (isIP(tlsName) !== 0) {
188
+ addIssue({
189
+ message: 'MSSQL_TLS_SERVER_NAME must be a hostname, not an IP address (Node.js TLS does not allow SNI with an IP).',
190
+ });
191
+ }
192
+ }
193
+ }
194
+ function rawEnvChecks(e, addIssue) {
195
+ const rawAuth = normalizeAuthType(e.MSSQL_AUTH_TYPE);
196
+ if (rawAuth !== undefined && !ALLOWED_AUTH.has(rawAuth)) {
197
+ addIssue({
198
+ 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.`,
199
+ });
200
+ return;
201
+ }
202
+ const authType = inferAuthType(e);
203
+ const user = (e.MSSQL_USER ?? '').trim();
204
+ if (authType === undefined) {
205
+ if (user === '') {
206
+ addIssue({
207
+ message: 'MSSQL_USER is required for SQL authentication (or set MSSQL_AUTH_TYPE).',
208
+ });
209
+ }
210
+ if (e.MSSQL_PASSWORD === undefined) {
211
+ addIssue({
212
+ message: 'MSSQL_PASSWORD is required for SQL authentication.',
213
+ });
214
+ }
215
+ return;
216
+ }
217
+ if (authType === 'ntlm') {
218
+ if (user === '')
219
+ addIssue({
220
+ message: 'MSSQL_USER is required when MSSQL_AUTH_TYPE=ntlm.',
221
+ });
222
+ if (e.MSSQL_PASSWORD === undefined) {
223
+ addIssue({
224
+ message: 'MSSQL_PASSWORD is required when MSSQL_AUTH_TYPE=ntlm.',
225
+ });
226
+ }
227
+ if (!e.MSSQL_DOMAIN?.trim())
228
+ addIssue({
229
+ message: 'MSSQL_DOMAIN is required when MSSQL_AUTH_TYPE=ntlm.',
230
+ });
231
+ return;
232
+ }
233
+ if (authType === 'azure-active-directory-password') {
234
+ if (user === '') {
235
+ addIssue({
236
+ message: 'MSSQL_USER is required when MSSQL_AUTH_TYPE=azure-active-directory-password.',
237
+ });
238
+ }
239
+ if (e.MSSQL_PASSWORD === undefined) {
240
+ addIssue({
241
+ message: 'MSSQL_PASSWORD is required when MSSQL_AUTH_TYPE=azure-active-directory-password.',
242
+ });
243
+ }
244
+ if (!e.MSSQL_AZURE_CLIENT_ID?.trim()) {
245
+ addIssue({
246
+ message: 'MSSQL_AZURE_CLIENT_ID is required when MSSQL_AUTH_TYPE=azure-active-directory-password.',
247
+ });
248
+ }
249
+ return;
250
+ }
251
+ if (authType === 'azure-active-directory-access-token') {
252
+ if (!e.MSSQL_AZURE_ACCESS_TOKEN?.trim()) {
253
+ addIssue({
254
+ message: 'MSSQL_AZURE_ACCESS_TOKEN is required when MSSQL_AUTH_TYPE=azure-active-directory-access-token.',
255
+ });
256
+ }
257
+ return;
258
+ }
259
+ if (authType === 'azure-active-directory-service-principal-secret') {
260
+ if (!e.MSSQL_AZURE_CLIENT_ID?.trim()) {
261
+ addIssue({
262
+ message: 'MSSQL_AZURE_CLIENT_ID is required when MSSQL_AUTH_TYPE=azure-active-directory-service-principal-secret.',
263
+ });
264
+ }
265
+ if (!e.MSSQL_AZURE_CLIENT_SECRET?.trim()) {
266
+ addIssue({
267
+ message: 'MSSQL_AZURE_CLIENT_SECRET is required when MSSQL_AUTH_TYPE=azure-active-directory-service-principal-secret.',
268
+ });
269
+ }
270
+ if (!e.MSSQL_AZURE_TENANT_ID?.trim()) {
271
+ addIssue({
272
+ message: 'MSSQL_AZURE_TENANT_ID is required when MSSQL_AUTH_TYPE=azure-active-directory-service-principal-secret.',
273
+ });
274
+ }
275
+ }
276
+ }
277
+ function rawPoolChecks(e, addIssue) {
278
+ const poolMaxRaw = e.MSSQL_POOL_MAX?.trim();
279
+ const poolMinRaw = e.MSSQL_POOL_MIN?.trim();
280
+ if ((poolMaxRaw !== undefined &&
281
+ poolMaxRaw !== '' &&
282
+ !INTEGER_PATTERN.test(poolMaxRaw)) ||
283
+ (poolMinRaw !== undefined &&
284
+ poolMinRaw !== '' &&
285
+ !INTEGER_PATTERN.test(poolMinRaw))) {
286
+ return;
287
+ }
288
+ const poolMax = parseIntEnv(e.MSSQL_POOL_MAX) ?? 10;
289
+ const poolMin = parseIntEnv(e.MSSQL_POOL_MIN) ?? 0;
290
+ if (poolMin > poolMax) {
291
+ addIssue({
292
+ message: `MSSQL_POOL_MIN (${poolMin}) cannot be greater than MSSQL_POOL_MAX (${poolMax}).`,
293
+ });
294
+ }
295
+ }
296
+ function rawTdsChecks(e, addIssue) {
297
+ const tdsRaw = e.MSSQL_TDS_VERSION?.trim();
298
+ if (tdsRaw === undefined || tdsRaw === '')
299
+ return;
300
+ if (!TDS_VERSIONS.has(tdsRaw)) {
301
+ addIssue({
302
+ message: `Invalid MSSQL_TDS_VERSION "${tdsRaw}". Use one of: ${[...TDS_VERSIONS].join(', ')}.`,
303
+ });
304
+ }
305
+ }
306
+ const appConfigSchema = v.pipe(envSchema, v.rawCheck(({ dataset, addIssue }) => {
307
+ rawScalarChecks(dataset.value, addIssue);
308
+ }), v.rawCheck(({ dataset, addIssue }) => {
309
+ rawEnvChecks(dataset.value, addIssue);
310
+ }), v.rawCheck(({ dataset, addIssue }) => {
311
+ rawPoolChecks(dataset.value, addIssue);
312
+ }), v.rawCheck(({ dataset, addIssue }) => {
313
+ rawTdsChecks(dataset.value, addIssue);
314
+ }), v.transform((e) => {
315
+ const authType = inferAuthType(e);
316
+ const port = parseIntEnv(e.MSSQL_PORT) ?? 1433;
317
+ const queryTimeoutValue = parseIntEnv(e.MSSQL_QUERY_TIMEOUT_MS);
318
+ const queryTimeoutMs = queryTimeoutValue === undefined || queryTimeoutValue === 0
319
+ ? undefined
320
+ : queryTimeoutValue;
321
+ const connectionTimeoutValue = parseIntEnv(e.MSSQL_CONNECTION_TIMEOUT_MS);
322
+ const connectionTimeoutMs = connectionTimeoutValue === undefined || connectionTimeoutValue === 0
323
+ ? undefined
324
+ : connectionTimeoutValue;
325
+ const ca = readPemFileOptional('MSSQL_TLS_CA_FILE', e.MSSQL_TLS_CA_FILE);
326
+ const cert = readPemFileOptional('MSSQL_TLS_CERT_FILE', e.MSSQL_TLS_CERT_FILE);
327
+ const key = readPemFileOptional('MSSQL_TLS_KEY_FILE', e.MSSQL_TLS_KEY_FILE);
328
+ const passphrase = e.MSSQL_TLS_KEY_PASSPHRASE?.trim() || undefined;
329
+ let cryptoCredentialsDetails;
330
+ if (ca !== undefined ||
331
+ cert !== undefined ||
332
+ key !== undefined ||
333
+ passphrase !== undefined) {
334
+ cryptoCredentialsDetails = {};
335
+ if (ca !== undefined)
336
+ cryptoCredentialsDetails.ca = ca;
337
+ if (cert !== undefined)
338
+ cryptoCredentialsDetails.cert = cert;
339
+ if (key !== undefined)
340
+ cryptoCredentialsDetails.key = key;
341
+ if (passphrase !== undefined)
342
+ cryptoCredentialsDetails.passphrase = passphrase;
343
+ }
344
+ const retriesValue = parseIntEnv(e.MSSQL_MAX_RETRIES_ON_TRANSIENT_ERRORS);
345
+ const maxRetriesOnTransientErrors = retriesValue === undefined || retriesValue === 0
346
+ ? undefined
347
+ : retriesValue;
348
+ const retryIntervalValue = parseIntEnv(e.MSSQL_CONNECTION_RETRY_INTERVAL_MS);
349
+ const connectionRetryIntervalMs = retryIntervalValue === undefined || retryIntervalValue === 0
350
+ ? undefined
351
+ : retryIntervalValue;
352
+ const tdsRaw = e.MSSQL_TDS_VERSION?.trim();
353
+ const tdsVersion = tdsRaw === '' || tdsRaw === undefined ? undefined : tdsRaw;
354
+ const instanceTrim = e.MSSQL_INSTANCE_NAME?.trim();
355
+ const instanceName = instanceTrim === '' ? undefined : instanceTrim;
356
+ const multiRaw = e.MSSQL_MULTI_SUBNET_FAILOVER;
357
+ const multiSubnetFailover = isUnset(multiRaw)
358
+ ? undefined
359
+ : parseBool(multiRaw, false);
360
+ const readOnlyRaw = e.MSSQL_READ_ONLY_INTENT;
361
+ const readOnlyIntent = isUnset(readOnlyRaw)
362
+ ? undefined
363
+ : parseBool(readOnlyRaw, false);
364
+ const useUtcRaw = e.MSSQL_USE_UTC;
365
+ const useUtc = isUnset(useUtcRaw) ? undefined : parseBool(useUtcRaw, true);
366
+ const poolMax = parseIntEnv(e.MSSQL_POOL_MAX) ?? 10;
367
+ const poolMin = parseIntEnv(e.MSSQL_POOL_MIN) ?? 0;
368
+ const poolIdleTimeoutMs = parseIntEnv(e.MSSQL_POOL_IDLE_TIMEOUT_MS) ?? 30_000;
369
+ const appTrim = e.MSSQL_APP_NAME?.trim();
370
+ const appName = appTrim === '' ? undefined : appTrim;
371
+ const tlsServerTrim = e.MSSQL_TLS_SERVER_NAME?.trim();
372
+ return {
373
+ server: e.MSSQL_SERVER.trim(),
374
+ port,
375
+ user: (e.MSSQL_USER ?? '').trim(),
376
+ password: e.MSSQL_PASSWORD ?? '',
377
+ database: e.MSSQL_DATABASE.trim(),
378
+ encrypt: parseEncrypt(e.MSSQL_ENCRYPT),
379
+ trustServerCertificate: parseBool(e.MSSQL_TRUST_SERVER_CERTIFICATE, false),
380
+ allowWrites: parseBool(e.MSSQL_ALLOW_WRITES, false),
381
+ queryTimeoutMs,
382
+ authType,
383
+ domain: e.MSSQL_DOMAIN?.trim() || undefined,
384
+ tlsServerName: tlsServerTrim === '' ? undefined : tlsServerTrim,
385
+ cryptoCredentialsDetails,
386
+ connectionTimeoutMs,
387
+ instanceName,
388
+ multiSubnetFailover,
389
+ readOnlyIntent,
390
+ maxRetriesOnTransientErrors,
391
+ connectionRetryIntervalMs,
392
+ poolMax,
393
+ poolMin,
394
+ poolIdleTimeoutMs,
395
+ appName,
396
+ useUtc,
397
+ tdsVersion,
398
+ azureClientId: e.MSSQL_AZURE_CLIENT_ID?.trim() || undefined,
399
+ azureTenantId: e.MSSQL_AZURE_TENANT_ID?.trim() || undefined,
400
+ azureClientSecret: e.MSSQL_AZURE_CLIENT_SECRET ?? undefined,
401
+ azureAccessToken: e.MSSQL_AZURE_ACCESS_TOKEN?.trim() || undefined,
402
+ };
403
+ }));
404
+ function readProcessEnv() {
405
+ return {
27
406
  MSSQL_SERVER: process.env.MSSQL_SERVER,
28
407
  MSSQL_USER: process.env.MSSQL_USER,
29
408
  MSSQL_PASSWORD: process.env.MSSQL_PASSWORD,
@@ -32,43 +411,128 @@ export function loadConfig() {
32
411
  MSSQL_ENCRYPT: process.env.MSSQL_ENCRYPT,
33
412
  MSSQL_TRUST_SERVER_CERTIFICATE: process.env.MSSQL_TRUST_SERVER_CERTIFICATE,
34
413
  MSSQL_ALLOW_WRITES: process.env.MSSQL_ALLOW_WRITES,
35
- MSSQL_MAX_ROWS: process.env.MSSQL_MAX_ROWS,
36
414
  MSSQL_QUERY_TIMEOUT_MS: process.env.MSSQL_QUERY_TIMEOUT_MS,
37
- });
38
- const port = parseIntEnv(e.MSSQL_PORT, 1433);
39
- const maxRowsRaw = e.MSSQL_MAX_ROWS;
40
- const maxRows = maxRowsRaw === undefined || maxRowsRaw === ''
41
- ? undefined
42
- : Math.max(0, parseIntEnv(maxRowsRaw, 0));
43
- const timeoutRaw = e.MSSQL_QUERY_TIMEOUT_MS;
44
- const queryTimeoutMs = timeoutRaw === undefined || timeoutRaw === ''
45
- ? undefined
46
- : Math.max(0, parseIntEnv(timeoutRaw, 0));
47
- return {
48
- server: e.MSSQL_SERVER,
49
- port,
50
- user: e.MSSQL_USER,
51
- password: e.MSSQL_PASSWORD,
52
- database: e.MSSQL_DATABASE,
53
- encrypt: parseBool(e.MSSQL_ENCRYPT, true),
54
- trustServerCertificate: parseBool(e.MSSQL_TRUST_SERVER_CERTIFICATE, false),
55
- allowWrites: parseBool(e.MSSQL_ALLOW_WRITES, false),
56
- maxRows: maxRows === 0 ? undefined : maxRows,
57
- queryTimeoutMs: queryTimeoutMs === 0 ? undefined : queryTimeoutMs,
415
+ MSSQL_TLS_SERVER_NAME: process.env.MSSQL_TLS_SERVER_NAME,
416
+ MSSQL_TLS_CA_FILE: process.env.MSSQL_TLS_CA_FILE,
417
+ MSSQL_TLS_CERT_FILE: process.env.MSSQL_TLS_CERT_FILE,
418
+ MSSQL_TLS_KEY_FILE: process.env.MSSQL_TLS_KEY_FILE,
419
+ MSSQL_TLS_KEY_PASSPHRASE: process.env.MSSQL_TLS_KEY_PASSPHRASE,
420
+ MSSQL_CONNECTION_TIMEOUT_MS: process.env.MSSQL_CONNECTION_TIMEOUT_MS,
421
+ MSSQL_DOMAIN: process.env.MSSQL_DOMAIN,
422
+ MSSQL_INSTANCE_NAME: process.env.MSSQL_INSTANCE_NAME,
423
+ MSSQL_MULTI_SUBNET_FAILOVER: process.env.MSSQL_MULTI_SUBNET_FAILOVER,
424
+ MSSQL_READ_ONLY_INTENT: process.env.MSSQL_READ_ONLY_INTENT,
425
+ MSSQL_MAX_RETRIES_ON_TRANSIENT_ERRORS: process.env.MSSQL_MAX_RETRIES_ON_TRANSIENT_ERRORS,
426
+ MSSQL_CONNECTION_RETRY_INTERVAL_MS: process.env.MSSQL_CONNECTION_RETRY_INTERVAL_MS,
427
+ MSSQL_POOL_MAX: process.env.MSSQL_POOL_MAX,
428
+ MSSQL_POOL_MIN: process.env.MSSQL_POOL_MIN,
429
+ MSSQL_POOL_IDLE_TIMEOUT_MS: process.env.MSSQL_POOL_IDLE_TIMEOUT_MS,
430
+ MSSQL_APP_NAME: process.env.MSSQL_APP_NAME,
431
+ MSSQL_USE_UTC: process.env.MSSQL_USE_UTC,
432
+ MSSQL_TDS_VERSION: process.env.MSSQL_TDS_VERSION,
433
+ MSSQL_AUTH_TYPE: process.env.MSSQL_AUTH_TYPE,
434
+ MSSQL_AZURE_CLIENT_ID: process.env.MSSQL_AZURE_CLIENT_ID,
435
+ MSSQL_AZURE_TENANT_ID: process.env.MSSQL_AZURE_TENANT_ID,
436
+ MSSQL_AZURE_CLIENT_SECRET: process.env.MSSQL_AZURE_CLIENT_SECRET,
437
+ MSSQL_AZURE_ACCESS_TOKEN: process.env.MSSQL_AZURE_ACCESS_TOKEN,
58
438
  };
59
439
  }
440
+ export function loadConfig() {
441
+ return v.parse(appConfigSchema, readProcessEnv());
442
+ }
60
443
  export function mssqlDriverConfig(cfg) {
61
- return {
62
- user: cfg.user,
63
- password: cfg.password,
444
+ const options = {
445
+ encrypt: cfg.encrypt,
446
+ trustServerCertificate: cfg.trustServerCertificate,
447
+ };
448
+ if (cfg.tlsServerName !== undefined)
449
+ options.serverName = cfg.tlsServerName;
450
+ if (cfg.instanceName !== undefined)
451
+ options.instanceName = cfg.instanceName;
452
+ if (cfg.multiSubnetFailover !== undefined)
453
+ options.multiSubnetFailover = cfg.multiSubnetFailover;
454
+ if (cfg.readOnlyIntent !== undefined)
455
+ options.readOnlyIntent = cfg.readOnlyIntent;
456
+ if (cfg.maxRetriesOnTransientErrors !== undefined) {
457
+ options.maxRetriesOnTransientErrors = cfg.maxRetriesOnTransientErrors;
458
+ }
459
+ if (cfg.connectionRetryIntervalMs !== undefined) {
460
+ options.connectionRetryInterval = cfg.connectionRetryIntervalMs;
461
+ }
462
+ if (cfg.cryptoCredentialsDetails !== undefined) {
463
+ options.cryptoCredentialsDetails = cfg.cryptoCredentialsDetails;
464
+ }
465
+ if (cfg.appName !== undefined)
466
+ options.appName = cfg.appName;
467
+ if (cfg.useUtc !== undefined)
468
+ options.useUTC = cfg.useUtc;
469
+ if (cfg.tdsVersion !== undefined) {
470
+ options.tdsVersion = cfg.tdsVersion;
471
+ }
472
+ let authentication;
473
+ if (cfg.authType === 'ntlm') {
474
+ authentication = {
475
+ type: 'ntlm',
476
+ options: {
477
+ userName: cfg.user,
478
+ password: cfg.password,
479
+ domain: cfg.domain,
480
+ },
481
+ };
482
+ }
483
+ else if (cfg.authType === 'azure-active-directory-password') {
484
+ authentication = {
485
+ type: 'azure-active-directory-password',
486
+ options: {
487
+ userName: cfg.user,
488
+ password: cfg.password,
489
+ clientId: cfg.azureClientId,
490
+ tenantId: cfg.azureTenantId ?? 'common',
491
+ },
492
+ };
493
+ }
494
+ else if (cfg.authType === 'azure-active-directory-access-token') {
495
+ authentication = {
496
+ type: 'azure-active-directory-access-token',
497
+ options: { token: cfg.azureAccessToken },
498
+ };
499
+ }
500
+ else if (cfg.authType === 'azure-active-directory-service-principal-secret') {
501
+ authentication = {
502
+ type: 'azure-active-directory-service-principal-secret',
503
+ options: {
504
+ clientId: cfg.azureClientId,
505
+ clientSecret: cfg.azureClientSecret,
506
+ tenantId: cfg.azureTenantId,
507
+ },
508
+ };
509
+ }
510
+ const base = {
64
511
  server: cfg.server,
65
- port: cfg.port,
66
512
  database: cfg.database,
67
- pool: { max: 10, min: 0, idleTimeoutMillis: 30_000 },
68
- options: {
69
- encrypt: cfg.encrypt,
70
- trustServerCertificate: cfg.trustServerCertificate,
513
+ pool: {
514
+ max: cfg.poolMax,
515
+ min: cfg.poolMin,
516
+ idleTimeoutMillis: cfg.poolIdleTimeoutMs,
71
517
  },
518
+ options,
72
519
  requestTimeout: cfg.queryTimeoutMs,
73
520
  };
521
+ if (cfg.connectionTimeoutMs !== undefined) {
522
+ base.connectionTimeout = cfg.connectionTimeoutMs;
523
+ }
524
+ if (cfg.instanceName === undefined) {
525
+ base.port = cfg.port;
526
+ }
527
+ if (authentication !== undefined) {
528
+ base.authentication = authentication;
529
+ }
530
+ else {
531
+ base.user = cfg.user;
532
+ base.password = cfg.password;
533
+ if (cfg.domain !== undefined) {
534
+ base.domain = cfg.domain;
535
+ }
536
+ }
537
+ return base;
74
538
  }
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, wrapWithRowCount } from './readonly-sql.js';
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 wrapped = wrapWithRowCount(batch, cfg.maxRows);
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
- const msg = err instanceof Error ? err.message : String(err);
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
  });
@@ -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;
@@ -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 Error('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).');
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.0",
3
+ "version": "1.0.2",
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
  }