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.
- package/README.md +7 -3
- package/dist/config.d.ts +82 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +225 -8
- package/dist/config.js.map +1 -1
- package/dist/index.js +17 -6
- package/dist/index.js.map +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/openapi.js +43 -8
- package/dist/openapi.js.map +1 -1
- package/dist/server.d.ts +2 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +130 -119
- package/dist/server.js.map +1 -1
- package/dist/tools/query.d.ts +5 -0
- package/dist/tools/query.d.ts.map +1 -1
- package/dist/tools/query.js +15 -2
- package/dist/tools/query.js.map +1 -1
- package/dist/tools/sqlLimit.d.ts +5 -0
- package/dist/tools/sqlLimit.d.ts.map +1 -1
- package/dist/tools/sqlLimit.js +12 -0
- package/dist/tools/sqlLimit.js.map +1 -1
- package/dist/transports/http.d.ts +1 -1
- package/dist/transports/http.d.ts.map +1 -1
- package/dist/transports/http.js +264 -194
- package/dist/transports/http.js.map +1 -1
- package/dist/transports/sessionManager.d.ts +5 -5
- package/dist/transports/sessionManager.d.ts.map +1 -1
- package/dist/transports/sessionManager.js +3 -3
- package/dist/transports/sessionManager.js.map +1 -1
- package/dist/utils/formatResult.d.ts +22 -0
- package/dist/utils/formatResult.d.ts.map +1 -0
- package/dist/utils/formatResult.js +66 -0
- package/dist/utils/formatResult.js.map +1 -0
- package/dist/utils/security/schemaAllowlist.d.ts +23 -0
- package/dist/utils/security/schemaAllowlist.d.ts.map +1 -0
- package/dist/utils/security/schemaAllowlist.js +146 -0
- package/dist/utils/security/schemaAllowlist.js.map +1 -0
- package/dist/utils/security/sqlSecurityValidator.d.ts +0 -4
- package/dist/utils/security/sqlSecurityValidator.d.ts.map +1 -1
- package/dist/utils/security/sqlSecurityValidator.js +134 -38
- package/dist/utils/security/sqlSecurityValidator.js.map +1 -1
- package/package.json +9 -6
package/dist/tools/query.js.map
CHANGED
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"query.js","sourceRoot":"","sources":["../../src/tools/query.ts"],"names":[],"mappings":"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"}
|
package/dist/tools/sqlLimit.d.ts
CHANGED
|
@@ -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"}
|
package/dist/tools/sqlLimit.js
CHANGED
|
@@ -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
|
-
* -
|
|
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;
|
|
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"}
|
package/dist/transports/http.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - OAuth-style token authentication (/auth)
|
|
6
6
|
* - MCP protocol endpoints (/mcp)
|
|
7
7
|
* - Health check endpoint (/health)
|
|
8
|
-
* -
|
|
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/
|
|
17
|
-
import {
|
|
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,
|
|
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 -
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
341
|
-
if (
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
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();
|