mcp-server-db2i 1.3.2 → 2.1.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.
Files changed (43) hide show
  1. package/README.md +7 -3
  2. package/dist/config.d.ts +82 -4
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +225 -8
  5. package/dist/config.js.map +1 -1
  6. package/dist/index.js +17 -6
  7. package/dist/index.js.map +1 -1
  8. package/dist/openapi.d.ts.map +1 -1
  9. package/dist/openapi.js +43 -8
  10. package/dist/openapi.js.map +1 -1
  11. package/dist/server.d.ts +2 -2
  12. package/dist/server.d.ts.map +1 -1
  13. package/dist/server.js +130 -119
  14. package/dist/server.js.map +1 -1
  15. package/dist/tools/query.d.ts +5 -0
  16. package/dist/tools/query.d.ts.map +1 -1
  17. package/dist/tools/query.js +15 -2
  18. package/dist/tools/query.js.map +1 -1
  19. package/dist/tools/sqlLimit.d.ts +5 -0
  20. package/dist/tools/sqlLimit.d.ts.map +1 -1
  21. package/dist/tools/sqlLimit.js +12 -0
  22. package/dist/tools/sqlLimit.js.map +1 -1
  23. package/dist/transports/http.d.ts +1 -1
  24. package/dist/transports/http.d.ts.map +1 -1
  25. package/dist/transports/http.js +264 -194
  26. package/dist/transports/http.js.map +1 -1
  27. package/dist/transports/sessionManager.d.ts +5 -5
  28. package/dist/transports/sessionManager.d.ts.map +1 -1
  29. package/dist/transports/sessionManager.js +3 -3
  30. package/dist/transports/sessionManager.js.map +1 -1
  31. package/dist/utils/formatResult.d.ts +22 -0
  32. package/dist/utils/formatResult.d.ts.map +1 -0
  33. package/dist/utils/formatResult.js +66 -0
  34. package/dist/utils/formatResult.js.map +1 -0
  35. package/dist/utils/security/schemaAllowlist.d.ts +23 -0
  36. package/dist/utils/security/schemaAllowlist.d.ts.map +1 -0
  37. package/dist/utils/security/schemaAllowlist.js +146 -0
  38. package/dist/utils/security/schemaAllowlist.js.map +1 -0
  39. package/dist/utils/security/sqlSecurityValidator.d.ts +0 -4
  40. package/dist/utils/security/sqlSecurityValidator.d.ts.map +1 -1
  41. package/dist/utils/security/sqlSecurityValidator.js +134 -38
  42. package/dist/utils/security/sqlSecurityValidator.js.map +1 -1
  43. package/package.json +9 -6
@@ -1 +1 @@
1
- {"version":3,"file":"query.js","sourceRoot":"","sources":["../../src/tools/query.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEjD,MAAM,GAAG,GAAG,iBAAiB,CAAC,EAAE,SAAS,EAAE,YAAY,EAAE,CAAC,CAAC;AAa3D;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,KAAwB;IAQ7D,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,EAAE,EAAE,SAAS,EAAE,GAAG,KAAK,CAAC;IAC9C,MAAM,WAAW,GAAG,mBAAmB,EAAE,CAAC;IAC1C,MAAM,cAAc,GAAG,eAAe,CAAC,KAAK,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC;IAEjE,GAAG,CAAC,KAAK,CACP,EAAE,UAAU,EAAE,GAAG,CAAC,SAAS,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,cAAc,EAAE,KAAK,CAAC,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EACzH,wBAAwB,CACzB,CAAC;IAEF,qEAAqE;IACrE,MAAM,gBAAgB,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;IAC5C,IAAI,CAAC,gBAAgB,CAAC,OAAO,EAAE,CAAC;QAC9B,GAAG,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,gBAAgB,CAAC,UAAU,EAAE,EAAE,4CAA4C,CAAC,CAAC;QACpG,OAAO;YACL,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,+BAA+B,gBAAgB,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;YAC9E,UAAU,EAAE,gBAAgB,CAAC,UAAU;SACxC,CAAC;IACJ,CAAC;IAED,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,gBAAgB,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;QACzD,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,UAAU,EAAE,MAAmB,EAAE,SAAS,CAAC,CAAC;QAC9E,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC;QAElD,GAAG,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,MAAM,EAAE,cAAc,EAAE,EAAE,6BAA6B,CAAC,CAAC;QACnF,OAAO;YACL,OAAO,EAAE,IAAI;YACb,IAAI,EAAE,IAAI;YACV,QAAQ,EAAE,IAAI,CAAC,MAAM;YACrB,YAAY,EAAE,cAAc;SAC7B,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,wBAAwB,CAAC;QAClF,GAAG,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,wBAAwB,CAAC,CAAC;QACpD,OAAO;YACL,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,OAAO;SACf,CAAC;IACJ,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"query.js","sourceRoot":"","sources":["../../src/tools/query.ts"],"names":[],"mappings":"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"}
@@ -9,5 +9,10 @@
9
9
  /**
10
10
  * Return SQL with a FETCH FIRST clause that does not exceed effectiveLimit.
11
11
  */
12
+ /**
13
+ * Remove a trailing FETCH FIRST or LIMIT clause, if one is present.
14
+ * Used to normalize SQL before parsing; it does not add a replacement clause.
15
+ */
16
+ export declare function stripTrailingRowLimit(sql: string): string;
12
17
  export declare function applySqlRowLimit(sql: string, effectiveLimit: number): string;
13
18
  //# sourceMappingURL=sqlLimit.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sqlLimit.d.ts","sourceRoot":"","sources":["../../src/tools/sqlLimit.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,MAAM,CAkB5E"}
1
+ {"version":3,"file":"sqlLimit.d.ts","sourceRoot":"","sources":["../../src/tools/sqlLimit.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH;;GAEG;AACH;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAOzD;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,MAAM,CAkB5E"}
@@ -10,6 +10,18 @@ const TRAILING_LIMIT_RE = /\b(?:FETCH\s+FIRST\s+(\d+)\s+ROWS?\s+ONLY|LIMIT\s+(\d
10
10
  /**
11
11
  * Return SQL with a FETCH FIRST clause that does not exceed effectiveLimit.
12
12
  */
13
+ /**
14
+ * Remove a trailing FETCH FIRST or LIMIT clause, if one is present.
15
+ * Used to normalize SQL before parsing; it does not add a replacement clause.
16
+ */
17
+ export function stripTrailingRowLimit(sql) {
18
+ const trimmed = sql.trim();
19
+ const match = TRAILING_LIMIT_RE.exec(trimmed);
20
+ if (match) {
21
+ return trimmed.slice(0, match.index).trimEnd();
22
+ }
23
+ return trimmed.endsWith(';') ? trimmed.slice(0, -1).trimEnd() : trimmed;
24
+ }
13
25
  export function applySqlRowLimit(sql, effectiveLimit) {
14
26
  const trimmed = sql.trim();
15
27
  const match = TRAILING_LIMIT_RE.exec(trimmed);
@@ -1 +1 @@
1
- {"version":3,"file":"sqlLimit.js","sourceRoot":"","sources":["../../src/tools/sqlLimit.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,iBAAiB,GACrB,oEAAoE,CAAC;AAEvE;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAW,EAAE,cAAsB;IAClE,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3B,MAAM,KAAK,GAAG,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAE9C,IAAI,KAAK,EAAE,CAAC;QACV,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;QACjE,MAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;YACvC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,cAAc,CAAC;YACpC,CAAC,CAAC,cAAc,CAAC;QACnB,MAAM,aAAa,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC;QAC9D,OAAO,GAAG,aAAa,gBAAgB,OAAO,YAAY,CAAC;IAC7D,CAAC;IAED,MAAM,gBAAgB,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC;QAC5C,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE;QAChC,CAAC,CAAC,OAAO,CAAC;IAEZ,OAAO,GAAG,gBAAgB,gBAAgB,cAAc,YAAY,CAAC;AACvE,CAAC"}
1
+ {"version":3,"file":"sqlLimit.js","sourceRoot":"","sources":["../../src/tools/sqlLimit.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,iBAAiB,GACrB,oEAAoE,CAAC;AAEvE;;GAEG;AACH;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CAAC,GAAW;IAC/C,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3B,MAAM,KAAK,GAAG,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC9C,IAAI,KAAK,EAAE,CAAC;QACV,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC;IACjD,CAAC;IACD,OAAO,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC;AAC1E,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,GAAW,EAAE,cAAsB;IAClE,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3B,MAAM,KAAK,GAAG,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAE9C,IAAI,KAAK,EAAE,CAAC;QACV,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;QACjE,MAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;YACvC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,cAAc,CAAC;YACpC,CAAC,CAAC,cAAc,CAAC;QACnB,MAAM,aAAa,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC;QAC9D,OAAO,GAAG,aAAa,gBAAgB,OAAO,YAAY,CAAC;IAC7D,CAAC;IAED,MAAM,gBAAgB,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC;QAC5C,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE;QAChC,CAAC,CAAC,OAAO,CAAC;IAEZ,OAAO,GAAG,gBAAgB,gBAAgB,cAAc,YAAY,CAAC;AACvE,CAAC"}
@@ -5,7 +5,7 @@
5
5
  * - OAuth-style token authentication (/auth)
6
6
  * - MCP protocol endpoints (/mcp)
7
7
  * - Health check endpoint (/health)
8
- * - Stateful and stateless session modes
8
+ * - Stateless MCP serving by default (stateful Mcp-Session-Id is deprecated)
9
9
  * - Optional TLS support
10
10
  */
11
11
  import { type Express } from 'express';
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/transports/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAgB,EAAE,KAAK,OAAO,EAA+B,MAAM,SAAS,CAAC;AAC7E,OAAO,KAAK,MAAM,YAAY,CAAC;AAC/B,OAAO,IAAI,MAAM,WAAW,CAAC;AA6G7B;;GAEG;AACH,wBAAgB,aAAa,IAAI,OAAO,CA8bvC;AAED;;GAEG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAqE3E;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA0B1F"}
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/transports/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAgB,EAAE,KAAK,OAAO,EAA+B,MAAM,SAAS,CAAC;AAC7E,OAAO,KAAK,MAAM,YAAY,CAAC;AAC/B,OAAO,IAAI,MAAM,WAAW,CAAC;AAqU7B;;GAEG;AACH,wBAAgB,aAAa,IAAI,OAAO,CA+SvC;AAED;;GAEG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAwF3E;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAiC1F"}
@@ -5,7 +5,7 @@
5
5
  * - OAuth-style token authentication (/auth)
6
6
  * - MCP protocol endpoints (/mcp)
7
7
  * - Health check endpoint (/health)
8
- * - Stateful and stateless session modes
8
+ * - Stateless MCP serving by default (stateful Mcp-Session-Id is deprecated)
9
9
  * - Optional TLS support
10
10
  */
11
11
  import express from 'express';
@@ -13,9 +13,9 @@ import https from 'node:https';
13
13
  import http from 'node:http';
14
14
  import crypto from 'node:crypto';
15
15
  import { readFileSync } from 'node:fs';
16
- import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
17
- import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
18
- import { getHttpConfig, loadConfig, loadPartialConfig, } from '../config.js';
16
+ import { createMcpHandler, isInitializeRequest, isLegacyRequest } from '@modelcontextprotocol/server';
17
+ import { toNodeHandler, toWebRequest } from '@modelcontextprotocol/node';
18
+ import { getHttpConfig, hostnameOf, isLoopbackHost, loadConfig, loadPartialConfig, } from '../config.js';
19
19
  import { createChildLogger } from '../utils/logger.js';
20
20
  import { getTokenManager, authMiddleware, authRateLimitMiddleware, recordFailedAuthAttempt, clearAuthRateLimit, } from '../auth/index.js';
21
21
  import { getSessionManager } from './sessionManager.js';
@@ -24,6 +24,8 @@ import { createServer as createMcpServer, SERVER_NAME, SERVER_VERSION } from '..
24
24
  import { getOpenApiSpec } from '../openapi.js';
25
25
  import { initializeSessionPool, testSessionConnection, closeSessionPool, closeAllSessionPools } from '../db/connection.js';
26
26
  const log = createChildLogger({ component: 'http-transport' });
27
+ /** Latest HTTP MCP handler, closed on shutdown so in-flight 2026 exchanges abort. */
28
+ let mcpHttpHandler;
27
29
  /**
28
30
  * Validate auth request body
29
31
  */
@@ -91,12 +93,215 @@ function sessionNotFoundBody() {
91
93
  id: null,
92
94
  };
93
95
  }
96
+ function bearerToken(authorization) {
97
+ if (!authorization) {
98
+ return undefined;
99
+ }
100
+ const [scheme, token] = authorization.split(' ');
101
+ if (!token || scheme?.toLowerCase() !== 'bearer') {
102
+ return undefined;
103
+ }
104
+ return token;
105
+ }
106
+ /**
107
+ * Build a per-request MCP server bound to the caller's database pool.
108
+ * Pools stay keyed by auth token (or the shared "global" key), not by MCP session id.
109
+ */
110
+ function createHttpMcpServer(request) {
111
+ const httpConfig = getHttpConfig();
112
+ let dbConfig;
113
+ let sessionKey;
114
+ if (httpConfig.authMode === 'none' || httpConfig.authMode === 'token') {
115
+ dbConfig = loadConfig();
116
+ sessionKey = resolveCallerSessionKey(httpConfig.authMode);
117
+ }
118
+ else {
119
+ const token = bearerToken(request?.headers.get('authorization'));
120
+ const validation = token ? getTokenManager().validateToken(token) : undefined;
121
+ if (!token || !validation?.valid || !validation.session) {
122
+ throw new Error('Token session not found');
123
+ }
124
+ dbConfig = validation.session.config;
125
+ sessionKey = resolveCallerSessionKey(httpConfig.authMode, token);
126
+ }
127
+ initializeSessionPool(sessionKey, dbConfig);
128
+ return createMcpServer(dbConfig, sessionKey);
129
+ }
130
+ /**
131
+ * Deprecated MCP_SESSION_MODE=stateful path for 2025-era clients that still
132
+ * send Mcp-Session-Id. 2026-07-28 traffic is not routed here.
133
+ */
134
+ async function handleStatefulLegacyRequest(req, res) {
135
+ const httpConfig = getHttpConfig();
136
+ const authReq = req;
137
+ if (req.method === 'GET') {
138
+ const sessionId = req.headers['mcp-session-id'];
139
+ if (!sessionId) {
140
+ res.status(400).json({
141
+ error: 'invalid_request',
142
+ error_description: 'Mcp-Session-Id header required',
143
+ });
144
+ return;
145
+ }
146
+ const sessionKey = resolveCallerSessionKey(httpConfig.authMode, authReq.authToken);
147
+ const sessionManager = getSessionManager();
148
+ const mcpSession = sessionManager.getSession(sessionId);
149
+ if (!mcpSession || !isSessionOwnedByCaller(mcpSession.authToken, sessionKey)) {
150
+ res.status(404).json({
151
+ error: 'not_found',
152
+ error_description: 'Session not found or expired',
153
+ });
154
+ return;
155
+ }
156
+ await mcpSession.transport.handleRequest(req, res);
157
+ return;
158
+ }
159
+ if (req.method === 'DELETE') {
160
+ const sessionId = req.headers['mcp-session-id'];
161
+ if (!sessionId) {
162
+ res.status(400).json({
163
+ error: 'invalid_request',
164
+ error_description: 'Mcp-Session-Id header required',
165
+ });
166
+ return;
167
+ }
168
+ const sessionKey = resolveCallerSessionKey(httpConfig.authMode, authReq.authToken);
169
+ const sessionManager = getSessionManager();
170
+ const mcpSession = sessionManager.getSession(sessionId);
171
+ if (!mcpSession || !isSessionOwnedByCaller(mcpSession.authToken, sessionKey)) {
172
+ res.status(404).json({
173
+ error: 'not_found',
174
+ error_description: 'Session not found',
175
+ });
176
+ return;
177
+ }
178
+ const closed = await sessionManager.closeSession(sessionId);
179
+ if (closed) {
180
+ res.json({ status: 'session_closed', sessionId });
181
+ }
182
+ else {
183
+ res.status(404).json({
184
+ error: 'not_found',
185
+ error_description: 'Session not found',
186
+ });
187
+ }
188
+ return;
189
+ }
190
+ if (req.method !== 'POST') {
191
+ res.status(405).json({
192
+ error: 'method_not_allowed',
193
+ error_description: 'Method not allowed',
194
+ });
195
+ return;
196
+ }
197
+ try {
198
+ let dbConfig;
199
+ let sessionKey;
200
+ if (httpConfig.authMode === 'none' || httpConfig.authMode === 'token') {
201
+ try {
202
+ dbConfig = loadConfig();
203
+ }
204
+ catch (err) {
205
+ const message = err instanceof Error ? err.message : 'Configuration error';
206
+ log.error({ err }, 'Failed to load DB config from environment');
207
+ res.status(500).json({
208
+ jsonrpc: '2.0',
209
+ error: { code: -32603, message: `DB configuration error: ${message}` },
210
+ id: null,
211
+ });
212
+ return;
213
+ }
214
+ sessionKey = resolveCallerSessionKey(httpConfig.authMode, authReq.authToken);
215
+ }
216
+ else {
217
+ if (!authReq.tokenSession || !authReq.authToken) {
218
+ log.error('Token session or auth token missing in required mode');
219
+ res.status(401).json({
220
+ jsonrpc: '2.0',
221
+ error: { code: -32001, message: 'Token session not found' },
222
+ id: null,
223
+ });
224
+ return;
225
+ }
226
+ dbConfig = authReq.tokenSession.config;
227
+ sessionKey = resolveCallerSessionKey(httpConfig.authMode, authReq.authToken);
228
+ }
229
+ const sessionId = req.headers['mcp-session-id'];
230
+ const sessionManager = getSessionManager();
231
+ if (sessionId) {
232
+ const mcpSession = sessionManager.getSession(sessionId);
233
+ if (!mcpSession || !isSessionOwnedByCaller(mcpSession.authToken, sessionKey)) {
234
+ res.status(404).json(sessionNotFoundBody());
235
+ return;
236
+ }
237
+ sessionManager.incrementActiveRequests(sessionId);
238
+ try {
239
+ await mcpSession.transport.handleRequest(req, res, req.body);
240
+ }
241
+ finally {
242
+ sessionManager.decrementActiveRequests(sessionId);
243
+ }
244
+ return;
245
+ }
246
+ if (!isInitializeRequest(req.body)) {
247
+ res.status(400).json({
248
+ jsonrpc: '2.0',
249
+ error: { code: -32000, message: 'Session ID required for non-initialize requests' },
250
+ id: null,
251
+ });
252
+ return;
253
+ }
254
+ initializeSessionPool(sessionKey, dbConfig);
255
+ let mcpServer;
256
+ let transport;
257
+ let newSessionId;
258
+ try {
259
+ mcpServer = createMcpServer(dbConfig, sessionKey);
260
+ const result = await sessionManager.createSession(mcpServer, sessionKey);
261
+ transport = result.transport;
262
+ newSessionId = result.sessionId;
263
+ }
264
+ catch (err) {
265
+ if (mcpServer) {
266
+ await mcpServer.close().catch(() => { });
267
+ }
268
+ if (sessionKey !== 'global') {
269
+ await closeSessionPool(sessionKey);
270
+ }
271
+ throw err;
272
+ }
273
+ if (httpConfig.authMode === 'required') {
274
+ getTokenManager().setMcpSessionId(sessionKey, newSessionId);
275
+ }
276
+ await transport.handleRequest(req, res, req.body);
277
+ }
278
+ catch (err) {
279
+ log.error({ err }, 'Error handling legacy MCP session request');
280
+ if (!res.headersSent) {
281
+ res.status(500).json({
282
+ jsonrpc: '2.0',
283
+ error: { code: -32603, message: 'Internal server error' },
284
+ id: null,
285
+ });
286
+ }
287
+ }
288
+ }
94
289
  /**
95
290
  * Create the Express application
96
291
  */
97
292
  export function createHttpApp() {
98
293
  const app = express();
99
294
  const httpConfig = getHttpConfig();
295
+ if (httpConfig.sessionMode === 'stateful') {
296
+ log.warn('MCP_SESSION_MODE=stateful is deprecated. Protocol sessions were removed in MCP 2026-07-28. ' +
297
+ 'The default stateless mode still isolates database pools by auth token. ' +
298
+ 'Stateful mode only keeps Mcp-Session-Id for 2025-era clients.');
299
+ }
300
+ mcpHttpHandler = createMcpHandler((ctx) => createHttpMcpServer(ctx.requestInfo), {
301
+ legacy: httpConfig.sessionMode === 'stateful' ? 'reject' : 'stateless',
302
+ onerror: (error) => log.error({ err: error }, 'MCP handler error'),
303
+ });
304
+ const mcpNodeHandler = toNodeHandler(mcpHttpHandler);
100
305
  // Middleware
101
306
  app.use(express.json());
102
307
  // Security headers
@@ -105,6 +310,25 @@ export function createHttpApp() {
105
310
  res.setHeader('X-Frame-Options', 'DENY');
106
311
  next();
107
312
  });
313
+ // Host allowlist. Runs before Origin checks so a rebinding request, which
314
+ // sends the attacker's name in both Host and Origin, is rejected first.
315
+ app.use((req, res, next) => {
316
+ const hostHeader = req.headers.host;
317
+ const hostname = typeof hostHeader === 'string' ? hostnameOf(hostHeader) : undefined;
318
+ if (!hostname || !httpConfig.allowedHosts.includes(hostname)) {
319
+ log.warn({
320
+ host: typeof hostHeader === 'string' ? hostHeader : undefined,
321
+ reason: 'host_not_allowed',
322
+ clientIp: req.socket.remoteAddress,
323
+ }, 'Rejected request');
324
+ res.status(403).json({
325
+ error: 'forbidden',
326
+ error_description: 'Forbidden: Host not allowed',
327
+ });
328
+ return;
329
+ }
330
+ next();
331
+ });
108
332
  // CORS and Origin validation (Streamable HTTP requires Origin checks)
109
333
  // By default (MCP_CORS_ORIGINS not set), only same-origin or missing Origin is allowed
110
334
  // Set MCP_CORS_ORIGINS='*' to allow all origins, or comma-separated list for specific origins
@@ -129,7 +353,7 @@ export function createHttpApp() {
129
353
  res.setHeader('Access-Control-Allow-Credentials', 'true');
130
354
  }
131
355
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
132
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Mcp-Session-Id, Accept');
356
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Accept, Mcp-Session-Id, MCP-Protocol-Version, Mcp-Method, Mcp-Name');
133
357
  }
134
358
  }
135
359
  if (req.method === 'OPTIONS') {
@@ -213,6 +437,16 @@ export function createHttpApp() {
213
437
  });
214
438
  return;
215
439
  }
440
+ const allowedDbHosts = httpConfig.authAllowedDbHosts;
441
+ const requestedHost = dbConfig.hostname.trim().replace(/\.$/, '').toLowerCase();
442
+ if (allowedDbHosts && !allowedDbHosts.includes(requestedHost)) {
443
+ recordFailedAuthAttempt(req);
444
+ res.status(400).json({
445
+ error: 'invalid_request',
446
+ error_description: 'Host is not allowed',
447
+ });
448
+ return;
449
+ }
216
450
  // Test connection to validate credentials
217
451
  log.debug({ host: dbConfig.hostname, user: dbConfig.username }, 'Testing credentials');
218
452
  // Use crypto random bytes for unique test pool ID (avoids collision with concurrent requests)
@@ -291,136 +525,19 @@ export function createHttpApp() {
291
525
  });
292
526
  }
293
527
  });
294
- // MCP endpoint - POST (main request handler)
295
- app.post('/mcp', authMiddleware, async (req, res) => {
296
- const authReq = req;
528
+ // MCP endpoint. Default serves 2026-07-28 per request and stateless 2025 clients.
529
+ // MCP_SESSION_MODE=stateful (deprecated) still routes claim-less 2025 traffic
530
+ // through the session manager.
531
+ app.all('/mcp', authMiddleware, async (req, res) => {
297
532
  try {
298
- // Determine DB config and session key based on auth mode
299
- let dbConfig;
300
- let sessionKey;
301
- if (httpConfig.authMode === 'none' || httpConfig.authMode === 'token') {
302
- // Use env-based config (like stdio mode)
303
- try {
304
- dbConfig = loadConfig();
305
- }
306
- catch (err) {
307
- const message = err instanceof Error ? err.message : 'Configuration error';
308
- log.error({ err }, 'Failed to load DB config from environment');
309
- res.status(500).json({
310
- jsonrpc: '2.0',
311
- error: { code: -32603, message: `DB configuration error: ${message}` },
312
- id: null,
313
- });
314
- return;
315
- }
316
- // Use a single shared pool for none/token modes (similar to stdio mode).
317
- // This pool persists for the server's lifetime and is cleaned up on shutdown
318
- // via closeAllSessionPools(). This is intentional connection reuse, not a leak.
319
- sessionKey = resolveCallerSessionKey(httpConfig.authMode, authReq.authToken);
320
- }
321
- else {
322
- // Required mode - use per-user config from token
323
- if (!authReq.tokenSession || !authReq.authToken) {
324
- log.error('Token session or auth token missing in required mode');
325
- res.status(401).json({
326
- jsonrpc: '2.0',
327
- error: { code: -32001, message: 'Token session not found' },
328
- id: null,
329
- });
330
- return;
331
- }
332
- const session = authReq.tokenSession;
333
- const authToken = authReq.authToken;
334
- dbConfig = session.config;
335
- sessionKey = resolveCallerSessionKey(httpConfig.authMode, authToken);
336
- }
337
- const sessionId = req.headers['mcp-session-id'];
338
- const sessionManager = getSessionManager();
339
533
  if (httpConfig.sessionMode === 'stateful') {
340
- // Stateful mode - reuse or create sessions
341
- if (sessionId) {
342
- // Existing session
343
- const mcpSession = sessionManager.getSession(sessionId);
344
- if (!mcpSession || !isSessionOwnedByCaller(mcpSession.authToken, sessionKey)) {
345
- res.status(404).json(sessionNotFoundBody());
346
- return;
347
- }
348
- sessionManager.incrementActiveRequests(sessionId);
349
- try {
350
- await mcpSession.transport.handleRequest(req, res, req.body);
351
- }
352
- finally {
353
- sessionManager.decrementActiveRequests(sessionId);
354
- }
355
- }
356
- else if (isInitializeRequest(req.body)) {
357
- // New session initialization
358
- // Initialize DB pool for this session key
359
- initializeSessionPool(sessionKey, dbConfig);
360
- // Create MCP server and session with proper cleanup on failure
361
- let mcpServer;
362
- let transport;
363
- let newSessionId;
364
- try {
365
- mcpServer = createMcpServer(dbConfig, sessionKey);
366
- const result = await sessionManager.createSession(mcpServer, sessionKey);
367
- transport = result.transport;
368
- newSessionId = result.sessionId;
369
- }
370
- catch (err) {
371
- // Clean up resources if server or session creation fails
372
- if (mcpServer) {
373
- await mcpServer.close().catch(() => { });
374
- }
375
- // Only close the pool if it's a per-user pool (required auth mode).
376
- // For 'none'/'token' modes, sessionKey='global' and the pool is shared
377
- // across all sessions - closing it would break other active sessions.
378
- // The global pool is only closed on server shutdown.
379
- if (sessionKey !== 'global') {
380
- await closeSessionPool(sessionKey);
381
- }
382
- throw err;
383
- }
384
- // Associate MCP session with token (only in required mode)
385
- if (httpConfig.authMode === 'required') {
386
- const tokenManager = getTokenManager();
387
- tokenManager.setMcpSessionId(sessionKey, newSessionId);
388
- }
389
- // Handle the initial request
390
- // Note: errors here are handled by the outer try-catch, and the session
391
- // will be cleaned up via normal session management (not here)
392
- await transport.handleRequest(req, res, req.body);
393
- }
394
- else {
395
- res.status(400).json({
396
- jsonrpc: '2.0',
397
- error: { code: -32000, message: 'Session ID required for non-initialize requests' },
398
- id: null,
399
- });
534
+ const probe = await toWebRequest(req, req.body);
535
+ if (await isLegacyRequest(probe)) {
536
+ await handleStatefulLegacyRequest(req, res);
537
+ return;
400
538
  }
401
539
  }
402
- else {
403
- // Stateless mode - new server per request
404
- const transport = new StreamableHTTPServerTransport({
405
- sessionIdGenerator: undefined,
406
- });
407
- // Initialize or reuse pool for this session key.
408
- // In 'required' auth mode, sessionKey = authToken, so the pool is shared
409
- // across all requests with the same token. This is intentional for efficiency.
410
- // Pool cleanup is handled by TokenManager when the token expires or is revoked
411
- // (see setCleanupCallback in startHttpServer).
412
- initializeSessionPool(sessionKey, dbConfig);
413
- const mcpServer = createMcpServer(dbConfig, sessionKey);
414
- // Clean up MCP server and transport on response close.
415
- // Note: The database connection pool is NOT closed here - it's reused across
416
- // requests with the same auth token and cleaned up on token expiration.
417
- res.on('close', () => {
418
- mcpServer.close().catch(() => { });
419
- transport.close().catch(() => { });
420
- });
421
- await mcpServer.connect(transport);
422
- await transport.handleRequest(req, res, req.body);
423
- }
540
+ await mcpNodeHandler(req, res, req.body);
424
541
  }
425
542
  catch (err) {
426
543
  log.error({ err }, 'Error handling MCP request');
@@ -433,69 +550,6 @@ export function createHttpApp() {
433
550
  }
434
551
  }
435
552
  });
436
- // MCP endpoint - GET (SSE for stateful mode)
437
- app.get('/mcp', authMiddleware, async (req, res) => {
438
- const authReq = req;
439
- const httpConfig = getHttpConfig();
440
- if (httpConfig.sessionMode !== 'stateful') {
441
- res.status(405).json({
442
- error: 'method_not_allowed',
443
- error_description: 'GET requests only supported in stateful mode',
444
- });
445
- return;
446
- }
447
- const sessionId = req.headers['mcp-session-id'];
448
- if (!sessionId) {
449
- res.status(400).json({
450
- error: 'invalid_request',
451
- error_description: 'Mcp-Session-Id header required',
452
- });
453
- return;
454
- }
455
- const sessionKey = resolveCallerSessionKey(httpConfig.authMode, authReq.authToken);
456
- const sessionManager = getSessionManager();
457
- const mcpSession = sessionManager.getSession(sessionId);
458
- if (!mcpSession || !isSessionOwnedByCaller(mcpSession.authToken, sessionKey)) {
459
- res.status(404).json({
460
- error: 'not_found',
461
- error_description: 'Session not found or expired',
462
- });
463
- return;
464
- }
465
- await mcpSession.transport.handleRequest(req, res);
466
- });
467
- // MCP endpoint - DELETE (close session)
468
- app.delete('/mcp', authMiddleware, async (req, res) => {
469
- const authReq = req;
470
- const sessionId = req.headers['mcp-session-id'];
471
- if (!sessionId) {
472
- res.status(400).json({
473
- error: 'invalid_request',
474
- error_description: 'Mcp-Session-Id header required',
475
- });
476
- return;
477
- }
478
- const sessionKey = resolveCallerSessionKey(httpConfig.authMode, authReq.authToken);
479
- const sessionManager = getSessionManager();
480
- const mcpSession = sessionManager.getSession(sessionId);
481
- if (!mcpSession || !isSessionOwnedByCaller(mcpSession.authToken, sessionKey)) {
482
- res.status(404).json({
483
- error: 'not_found',
484
- error_description: 'Session not found',
485
- });
486
- return;
487
- }
488
- const closed = await sessionManager.closeSession(sessionId);
489
- if (closed) {
490
- res.json({ status: 'session_closed', sessionId });
491
- }
492
- else {
493
- res.status(404).json({
494
- error: 'not_found',
495
- error_description: 'Session not found',
496
- });
497
- }
498
- });
499
553
  return app;
500
554
  }
501
555
  /**
@@ -503,6 +557,16 @@ export function createHttpApp() {
503
557
  */
504
558
  export async function startHttpServer() {
505
559
  const httpConfig = getHttpConfig();
560
+ if (!isLoopbackHost(httpConfig.host) &&
561
+ httpConfig.authMode === 'none' &&
562
+ !httpConfig.allowUnauthenticatedHttp) {
563
+ throw new Error(`Refusing to listen on ${httpConfig.host} with MCP_AUTH_MODE=none. ` +
564
+ 'Enable authentication, bind a loopback address, or set MCP_ALLOW_UNAUTHENTICATED_HTTP=true.');
565
+ }
566
+ if (httpConfig.authMode === 'required' && httpConfig.authAllowedDbHosts === null) {
567
+ log.warn('MCP_AUTH_ALLOWED_DB_HOSTS and DB2I_HOSTNAME are unset. ' +
568
+ '/auth will open a database connection to any host the client names.');
569
+ }
506
570
  const app = createHttpApp();
507
571
  // Register cleanup callback to close session pools when tokens expire or are revoked.
508
572
  // This handles cleanup for both stateful and stateless modes in 'required' auth:
@@ -561,6 +625,12 @@ export async function startHttpServer() {
561
625
  */
562
626
  export async function shutdownHttpServer(server) {
563
627
  log.info('Shutting down HTTP server...');
628
+ if (mcpHttpHandler) {
629
+ await mcpHttpHandler.close().catch((err) => {
630
+ log.error({ err }, 'Error closing MCP HTTP handler');
631
+ });
632
+ mcpHttpHandler = undefined;
633
+ }
564
634
  // Close session manager (closes MCP sessions)
565
635
  const sessionManager = getSessionManager();
566
636
  await sessionManager.shutdown();