@j0hanz/superfetch 1.2.5 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +131 -156
- package/dist/config/auth-config.d.ts +16 -0
- package/dist/config/auth-config.js +53 -0
- package/dist/config/constants.d.ts +11 -13
- package/dist/config/constants.js +1 -3
- package/dist/config/env-parsers.d.ts +7 -0
- package/dist/config/env-parsers.js +84 -0
- package/dist/config/formatting.d.ts +2 -2
- package/dist/config/index.d.ts +47 -53
- package/dist/config/index.js +35 -64
- package/dist/config/types/content.d.ts +1 -49
- package/dist/config/types/runtime.d.ts +8 -16
- package/dist/config/types/tools.d.ts +2 -28
- package/dist/http/accept-policy.d.ts +3 -0
- package/dist/http/accept-policy.js +45 -0
- package/dist/http/async-handler.d.ts +2 -0
- package/dist/http/async-handler.js +5 -0
- package/dist/http/auth-introspection.d.ts +2 -0
- package/dist/http/auth-introspection.js +141 -0
- package/dist/http/auth-static.d.ts +2 -0
- package/dist/http/auth-static.js +23 -0
- package/dist/http/auth.d.ts +3 -2
- package/dist/http/auth.js +254 -23
- package/dist/http/cors.d.ts +6 -6
- package/dist/http/cors.js +7 -42
- package/dist/http/download-routes.d.ts +0 -12
- package/dist/http/download-routes.js +21 -58
- package/dist/http/host-allowlist.d.ts +3 -0
- package/dist/http/host-allowlist.js +117 -0
- package/dist/http/jsonrpc-http.d.ts +2 -0
- package/dist/http/jsonrpc-http.js +10 -0
- package/dist/http/mcp-routes.d.ts +8 -3
- package/dist/http/mcp-routes.js +137 -31
- package/dist/http/mcp-session-eviction.d.ts +3 -0
- package/dist/http/mcp-session-eviction.js +24 -0
- package/dist/http/mcp-session-helpers.d.ts +0 -1
- package/dist/http/mcp-session-helpers.js +1 -1
- package/dist/http/mcp-session-init.d.ts +7 -0
- package/dist/http/mcp-session-init.js +94 -0
- package/dist/http/mcp-session-slots.d.ts +17 -0
- package/dist/http/mcp-session-slots.js +55 -0
- package/dist/http/mcp-session-transport-init.d.ts +7 -0
- package/dist/http/mcp-session-transport-init.js +41 -0
- package/dist/http/mcp-session-transport.d.ts +7 -0
- package/dist/http/mcp-session-transport.js +57 -0
- package/dist/http/mcp-session-types.d.ts +5 -0
- package/dist/http/mcp-session-types.js +1 -0
- package/dist/http/mcp-session.d.ts +9 -9
- package/dist/http/mcp-session.js +15 -137
- package/dist/http/mcp-sessions.d.ts +43 -0
- package/dist/http/mcp-sessions.js +392 -0
- package/dist/http/mcp-validation.d.ts +1 -0
- package/dist/http/mcp-validation.js +11 -10
- package/dist/http/protocol-policy.d.ts +2 -0
- package/dist/http/protocol-policy.js +31 -0
- package/dist/http/rate-limit.js +7 -4
- package/dist/http/server-config.d.ts +1 -0
- package/dist/http/server-config.js +40 -0
- package/dist/http/server-middleware.d.ts +7 -9
- package/dist/http/server-middleware.js +9 -70
- package/dist/http/server-shutdown.d.ts +4 -0
- package/dist/http/server-shutdown.js +43 -0
- package/dist/http/server.d.ts +10 -0
- package/dist/http/server.js +546 -61
- package/dist/http/session-cleanup.js +8 -5
- package/dist/middleware/error-handler.d.ts +1 -1
- package/dist/middleware/error-handler.js +32 -33
- package/dist/resources/cached-content-params.d.ts +5 -0
- package/dist/resources/cached-content-params.js +36 -0
- package/dist/resources/cached-content.js +67 -125
- package/dist/resources/index.js +0 -82
- package/dist/server.js +50 -29
- package/dist/services/cache-events.d.ts +8 -0
- package/dist/services/cache-events.js +19 -0
- package/dist/services/cache-keys.d.ts +7 -0
- package/dist/services/cache-keys.js +57 -0
- package/dist/services/cache.d.ts +4 -9
- package/dist/services/cache.js +77 -139
- package/dist/services/context.d.ts +0 -1
- package/dist/services/context.js +0 -7
- package/dist/services/extractor.js +55 -116
- package/dist/services/fetcher/agents.d.ts +2 -2
- package/dist/services/fetcher/agents.js +35 -96
- package/dist/services/fetcher/dns-selection.d.ts +2 -0
- package/dist/services/fetcher/dns-selection.js +72 -0
- package/dist/services/fetcher/interceptors.d.ts +0 -22
- package/dist/services/fetcher/interceptors.js +18 -32
- package/dist/services/fetcher/redirects.js +16 -7
- package/dist/services/fetcher/response.js +79 -34
- package/dist/services/fetcher.d.ts +22 -3
- package/dist/services/fetcher.js +544 -44
- package/dist/services/fifo-queue.d.ts +8 -0
- package/dist/services/fifo-queue.js +25 -0
- package/dist/services/logger.js +2 -2
- package/dist/services/metadata-collector.d.ts +1 -9
- package/dist/services/metadata-collector.js +71 -2
- package/dist/services/transform-worker-pool.d.ts +4 -14
- package/dist/services/transform-worker-pool.js +177 -129
- package/dist/services/transform-worker-types.d.ts +32 -0
- package/dist/services/transform-worker-types.js +14 -0
- package/dist/tools/handlers/fetch-markdown.tool.d.ts +3 -4
- package/dist/tools/handlers/fetch-markdown.tool.js +20 -72
- package/dist/tools/handlers/fetch-single.shared.d.ts +11 -22
- package/dist/tools/handlers/fetch-single.shared.js +175 -89
- package/dist/tools/handlers/fetch-url.tool.d.ts +7 -1
- package/dist/tools/handlers/fetch-url.tool.js +84 -119
- package/dist/tools/index.js +21 -40
- package/dist/tools/schemas.d.ts +1 -51
- package/dist/tools/schemas.js +1 -107
- package/dist/tools/utils/cached-markdown.d.ts +5 -0
- package/dist/tools/utils/cached-markdown.js +46 -0
- package/dist/tools/utils/content-shaping.d.ts +4 -0
- package/dist/tools/utils/content-shaping.js +67 -0
- package/dist/tools/utils/content-transform.d.ts +5 -17
- package/dist/tools/utils/content-transform.js +134 -114
- package/dist/tools/utils/fetch-pipeline.d.ts +0 -8
- package/dist/tools/utils/fetch-pipeline.js +57 -63
- package/dist/tools/utils/frontmatter.d.ts +3 -0
- package/dist/tools/utils/frontmatter.js +73 -0
- package/dist/tools/utils/inline-content.d.ts +1 -2
- package/dist/tools/utils/inline-content.js +4 -7
- package/dist/tools/utils/markdown-heuristics.d.ts +1 -0
- package/dist/tools/utils/markdown-heuristics.js +19 -0
- package/dist/tools/utils/markdown-signals.d.ts +1 -0
- package/dist/tools/utils/markdown-signals.js +19 -0
- package/dist/tools/utils/raw-markdown-frontmatter.d.ts +3 -0
- package/dist/tools/utils/raw-markdown-frontmatter.js +73 -0
- package/dist/tools/utils/raw-markdown.d.ts +6 -0
- package/dist/tools/utils/raw-markdown.js +135 -0
- package/dist/transformers/markdown/fenced-code-rule.d.ts +2 -0
- package/dist/transformers/markdown/fenced-code-rule.js +38 -0
- package/dist/transformers/markdown/frontmatter.d.ts +2 -0
- package/dist/transformers/markdown/frontmatter.js +45 -0
- package/dist/transformers/markdown/noise-rule.d.ts +2 -0
- package/dist/transformers/markdown/noise-rule.js +80 -0
- package/dist/transformers/markdown/turndown-instance.d.ts +2 -0
- package/dist/transformers/markdown/turndown-instance.js +19 -0
- package/dist/transformers/markdown.d.ts +2 -0
- package/dist/transformers/markdown.js +185 -0
- package/dist/transformers/markdown.transformer.js +5 -117
- package/dist/utils/cached-payload.d.ts +7 -0
- package/dist/utils/cached-payload.js +36 -0
- package/dist/utils/code-language-bash.d.ts +1 -0
- package/dist/utils/code-language-bash.js +48 -0
- package/dist/utils/code-language-core.d.ts +2 -0
- package/dist/utils/code-language-core.js +13 -0
- package/dist/utils/code-language-detectors.d.ts +5 -0
- package/dist/utils/code-language-detectors.js +142 -0
- package/dist/utils/code-language-helpers.d.ts +5 -0
- package/dist/utils/code-language-helpers.js +62 -0
- package/dist/utils/code-language-parsing.d.ts +5 -0
- package/dist/utils/code-language-parsing.js +62 -0
- package/dist/utils/code-language.d.ts +9 -0
- package/dist/utils/code-language.js +250 -46
- package/dist/utils/error-details.d.ts +3 -0
- package/dist/utils/error-details.js +12 -0
- package/dist/utils/error-utils.js +1 -1
- package/dist/utils/filename-generator.js +34 -12
- package/dist/utils/guards.d.ts +1 -0
- package/dist/utils/guards.js +3 -0
- package/dist/utils/header-normalizer.d.ts +0 -3
- package/dist/utils/header-normalizer.js +3 -3
- package/dist/utils/ip-address.d.ts +4 -0
- package/dist/utils/ip-address.js +6 -0
- package/dist/utils/tool-error-handler.d.ts +2 -2
- package/dist/utils/tool-error-handler.js +14 -46
- package/dist/utils/url-transformer.d.ts +7 -0
- package/dist/utils/url-transformer.js +147 -0
- package/dist/utils/url-validator.d.ts +1 -2
- package/dist/utils/url-validator.js +53 -114
- package/dist/workers/content-transform.worker.d.ts +1 -0
- package/dist/workers/content-transform.worker.js +40 -0
- package/package.json +17 -18
package/dist/http/mcp-routes.js
CHANGED
|
@@ -1,31 +1,48 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import { logError, logInfo } from '../services/logger.js';
|
|
2
|
-
import { resolveTransportForPost, } from './mcp-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
3
|
+
import { getSessionId, resolveTransportForPost, sendJsonRpcError, } from './mcp-sessions.js';
|
|
4
|
+
const paramsSchema = z.looseObject({});
|
|
5
|
+
const mcpRequestSchema = z.looseObject({
|
|
6
|
+
jsonrpc: z.literal('2.0'),
|
|
7
|
+
method: z.string().min(1),
|
|
8
|
+
id: z.union([z.string(), z.number()]).optional(),
|
|
9
|
+
params: paramsSchema.optional(),
|
|
10
|
+
});
|
|
11
|
+
function wrapAsync(fn) {
|
|
12
|
+
return (req, res, next) => {
|
|
13
|
+
Promise.resolve(fn(req, res)).catch(next);
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function isJsonRpcBatchRequest(body) {
|
|
17
|
+
return Array.isArray(body);
|
|
18
|
+
}
|
|
19
|
+
export function isMcpRequestBody(body) {
|
|
20
|
+
return mcpRequestSchema.safeParse(body).success;
|
|
14
21
|
}
|
|
15
22
|
function respondInvalidRequestBody(res) {
|
|
16
23
|
sendJsonRpcError(res, -32600, 'Invalid Request: Malformed request body', 400);
|
|
17
24
|
}
|
|
18
25
|
function respondMissingSession(res) {
|
|
19
|
-
res
|
|
26
|
+
sendJsonRpcError(res, -32600, 'Missing mcp-session-id header', 400);
|
|
20
27
|
}
|
|
21
28
|
function respondSessionNotFound(res) {
|
|
22
|
-
res
|
|
29
|
+
sendJsonRpcError(res, -32600, 'Session not found', 404);
|
|
30
|
+
}
|
|
31
|
+
function validatePostPayload(payload, res) {
|
|
32
|
+
if (isJsonRpcBatchRequest(payload)) {
|
|
33
|
+
sendJsonRpcError(res, -32600, 'Batch requests are not supported', 400);
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
if (!isMcpRequestBody(payload)) {
|
|
37
|
+
respondInvalidRequestBody(res);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return payload;
|
|
23
41
|
}
|
|
24
42
|
function logPostRequest(body, sessionId, options) {
|
|
25
43
|
logInfo('[MCP POST]', {
|
|
26
44
|
method: body.method,
|
|
27
45
|
id: body.id,
|
|
28
|
-
sessionId: sessionId ?? 'none',
|
|
29
46
|
isInitialize: body.method === 'initialize',
|
|
30
47
|
sessionCount: options.sessionStore.size(),
|
|
31
48
|
});
|
|
@@ -50,49 +67,138 @@ function dispatchTransportRequest(transport, req, res, body) {
|
|
|
50
67
|
: transport.handleRequest(req, res);
|
|
51
68
|
}
|
|
52
69
|
function resolveSessionTransport(sessionId, options, res) {
|
|
70
|
+
const { sessionStore } = options;
|
|
53
71
|
if (!sessionId) {
|
|
54
72
|
respondMissingSession(res);
|
|
55
73
|
return null;
|
|
56
74
|
}
|
|
57
|
-
const session =
|
|
75
|
+
const session = sessionStore.get(sessionId);
|
|
58
76
|
if (!session) {
|
|
59
77
|
respondSessionNotFound(res);
|
|
60
78
|
return null;
|
|
61
79
|
}
|
|
62
|
-
|
|
80
|
+
sessionStore.touch(sessionId);
|
|
63
81
|
return session.transport;
|
|
64
82
|
}
|
|
83
|
+
const MCP_PROTOCOL_VERSION_HEADER = 'mcp-protocol-version';
|
|
84
|
+
const MCP_PROTOCOL_VERSIONS = {
|
|
85
|
+
defaultVersion: '2025-03-26',
|
|
86
|
+
supported: new Set(['2025-03-26', '2025-11-25']),
|
|
87
|
+
};
|
|
88
|
+
function getHeaderValue(req, headerNameLower) {
|
|
89
|
+
const value = req.headers[headerNameLower];
|
|
90
|
+
if (typeof value === 'string')
|
|
91
|
+
return value;
|
|
92
|
+
if (Array.isArray(value))
|
|
93
|
+
return value[0] ?? null;
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
function setHeaderValue(req, headerNameLower, value) {
|
|
97
|
+
// Express exposes req.headers as a plain object, but the type is readonly-ish.
|
|
98
|
+
req.headers[headerNameLower] = value;
|
|
99
|
+
}
|
|
100
|
+
export function ensureMcpProtocolVersionHeader(req, res) {
|
|
101
|
+
const raw = getHeaderValue(req, MCP_PROTOCOL_VERSION_HEADER);
|
|
102
|
+
const version = raw?.trim();
|
|
103
|
+
if (!version) {
|
|
104
|
+
setHeaderValue(req, MCP_PROTOCOL_VERSION_HEADER, MCP_PROTOCOL_VERSIONS.defaultVersion);
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
if (!MCP_PROTOCOL_VERSIONS.supported.has(version)) {
|
|
108
|
+
sendJsonRpcError(res, -32600, `Unsupported MCP-Protocol-Version: ${version}`, 400);
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
function getAcceptHeader(req) {
|
|
114
|
+
const value = req.headers.accept;
|
|
115
|
+
if (typeof value === 'string')
|
|
116
|
+
return value;
|
|
117
|
+
return '';
|
|
118
|
+
}
|
|
119
|
+
function setAcceptHeader(req, value) {
|
|
120
|
+
req.headers.accept = value;
|
|
121
|
+
const { rawHeaders } = req;
|
|
122
|
+
if (!Array.isArray(rawHeaders))
|
|
123
|
+
return;
|
|
124
|
+
for (let i = 0; i + 1 < rawHeaders.length; i += 2) {
|
|
125
|
+
const key = rawHeaders[i];
|
|
126
|
+
if (typeof key === 'string' && key.toLowerCase() === 'accept') {
|
|
127
|
+
rawHeaders[i + 1] = value;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
rawHeaders.push('Accept', value);
|
|
132
|
+
}
|
|
133
|
+
function hasToken(header, token) {
|
|
134
|
+
return header
|
|
135
|
+
.split(',')
|
|
136
|
+
.map((part) => part.trim().toLowerCase())
|
|
137
|
+
.some((part) => part === token || part.startsWith(`${token};`));
|
|
138
|
+
}
|
|
139
|
+
export function ensurePostAcceptHeader(req) {
|
|
140
|
+
const accept = getAcceptHeader(req);
|
|
141
|
+
// Some clients send */* or omit Accept; the SDK transport is picky.
|
|
142
|
+
if (!accept || hasToken(accept, '*/*')) {
|
|
143
|
+
setAcceptHeader(req, 'application/json, text/event-stream');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const hasJson = hasToken(accept, 'application/json');
|
|
147
|
+
const hasSse = hasToken(accept, 'text/event-stream');
|
|
148
|
+
if (!hasJson || !hasSse) {
|
|
149
|
+
setAcceptHeader(req, 'application/json, text/event-stream');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
export function acceptsEventStream(req) {
|
|
153
|
+
const accept = getAcceptHeader(req);
|
|
154
|
+
if (!accept)
|
|
155
|
+
return false;
|
|
156
|
+
return hasToken(accept, 'text/event-stream');
|
|
157
|
+
}
|
|
65
158
|
async function handlePost(req, res, options) {
|
|
159
|
+
ensurePostAcceptHeader(req);
|
|
160
|
+
if (!ensureMcpProtocolVersionHeader(req, res))
|
|
161
|
+
return;
|
|
66
162
|
const sessionId = getSessionId(req);
|
|
67
|
-
const
|
|
68
|
-
if (!
|
|
69
|
-
respondInvalidRequestBody(res);
|
|
163
|
+
const payload = validatePostPayload(req.body, res);
|
|
164
|
+
if (!payload)
|
|
70
165
|
return;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
166
|
+
logPostRequest(payload, sessionId, options);
|
|
167
|
+
const transport = await resolveTransportForPost({
|
|
168
|
+
res,
|
|
169
|
+
body: payload,
|
|
170
|
+
sessionId,
|
|
171
|
+
options,
|
|
172
|
+
});
|
|
74
173
|
if (!transport)
|
|
75
174
|
return;
|
|
76
|
-
await handleTransportRequest(transport, req, res,
|
|
175
|
+
await handleTransportRequest(transport, req, res, payload);
|
|
77
176
|
}
|
|
78
177
|
async function handleGet(req, res, options) {
|
|
178
|
+
if (!ensureMcpProtocolVersionHeader(req, res))
|
|
179
|
+
return;
|
|
180
|
+
if (!acceptsEventStream(req)) {
|
|
181
|
+
res.status(406).json({
|
|
182
|
+
error: 'Not Acceptable',
|
|
183
|
+
code: 'ACCEPT_NOT_SUPPORTED',
|
|
184
|
+
});
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
79
187
|
const transport = resolveSessionTransport(getSessionId(req), options, res);
|
|
80
188
|
if (!transport)
|
|
81
189
|
return;
|
|
82
190
|
await handleTransportRequest(transport, req, res);
|
|
83
191
|
}
|
|
84
192
|
async function handleDelete(req, res, options) {
|
|
193
|
+
if (!ensureMcpProtocolVersionHeader(req, res))
|
|
194
|
+
return;
|
|
85
195
|
const transport = resolveSessionTransport(getSessionId(req), options, res);
|
|
86
196
|
if (!transport)
|
|
87
197
|
return;
|
|
88
198
|
await handleTransportRequest(transport, req, res);
|
|
89
199
|
}
|
|
90
200
|
export function registerMcpRoutes(app, options) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
app.post('/mcp', asyncHandler((req, res) => handlePost(req, res, options)));
|
|
95
|
-
app.get('/mcp', asyncHandler((req, res) => handleGet(req, res, options)));
|
|
96
|
-
app.delete('/mcp', asyncHandler((req, res) => handleDelete(req, res, options)));
|
|
201
|
+
app.post('/mcp', wrapAsync((req, res) => handlePost(req, res, options)));
|
|
202
|
+
app.get('/mcp', wrapAsync((req, res) => handleGet(req, res, options)));
|
|
203
|
+
app.delete('/mcp', wrapAsync((req, res) => handleDelete(req, res, options)));
|
|
97
204
|
}
|
|
98
|
-
export { evictExpiredSessions } from './mcp-session.js';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { logWarn } from '../services/logger.js';
|
|
2
|
+
import { getErrorMessage } from '../utils/error-details.js';
|
|
3
|
+
export function evictExpiredSessions(store) {
|
|
4
|
+
const evicted = store.evictExpired();
|
|
5
|
+
for (const session of evicted) {
|
|
6
|
+
void session.transport.close().catch((error) => {
|
|
7
|
+
logWarn('Failed to close expired session', {
|
|
8
|
+
error: getErrorMessage(error),
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
return evicted.length;
|
|
13
|
+
}
|
|
14
|
+
export function evictOldestSession(store) {
|
|
15
|
+
const session = store.evictOldest();
|
|
16
|
+
if (!session)
|
|
17
|
+
return false;
|
|
18
|
+
void session.transport.close().catch((error) => {
|
|
19
|
+
logWarn('Failed to close evicted session', {
|
|
20
|
+
error: getErrorMessage(error),
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
@@ -6,7 +6,6 @@ export interface SlotTracker {
|
|
|
6
6
|
readonly isInitialized: () => boolean;
|
|
7
7
|
}
|
|
8
8
|
export declare function reserveSessionSlot(store: SessionStore, maxSessions: number): boolean;
|
|
9
|
-
export declare function releaseSessionSlot(): void;
|
|
10
9
|
export declare function createSlotTracker(): SlotTracker;
|
|
11
10
|
export declare function ensureSessionCapacity(store: SessionStore, maxSessions: number, res: Response, evictOldest: (store: SessionStore) => boolean): boolean;
|
|
12
11
|
export declare function respondServerBusy(res: Response): void;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Response } from 'express';
|
|
2
|
+
import type { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
3
|
+
import type { McpSessionOptions } from './mcp-session-types.js';
|
|
4
|
+
export declare function createAndConnectTransport({ options, res, }: {
|
|
5
|
+
options: McpSessionOptions;
|
|
6
|
+
res: Response;
|
|
7
|
+
}): Promise<StreamableHTTPServerTransport | null>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { logError, logInfo, logWarn } from '../services/logger.js';
|
|
2
|
+
import { getErrorMessage } from '../utils/error-details.js';
|
|
3
|
+
import { createMcpServer } from '../server.js';
|
|
4
|
+
import { evictOldestSession } from './mcp-session-eviction.js';
|
|
5
|
+
import { createSlotTracker, ensureSessionCapacity, reserveSessionSlot, respondBadRequest, respondServerBusy, } from './mcp-session-slots.js';
|
|
6
|
+
import { createSessionTransport } from './mcp-session-transport-init.js';
|
|
7
|
+
import { createTimeoutController, createTransportAdapter, } from './mcp-session-transport.js';
|
|
8
|
+
async function connectTransportOrThrow({ transport, clearInitTimeout, releaseSlot, }) {
|
|
9
|
+
const mcpServer = createMcpServer();
|
|
10
|
+
const transportAdapter = createTransportAdapter(transport);
|
|
11
|
+
try {
|
|
12
|
+
await mcpServer.connect(transportAdapter);
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
clearInitTimeout();
|
|
16
|
+
releaseSlot();
|
|
17
|
+
void transport.close().catch((closeError) => {
|
|
18
|
+
logWarn('Failed to close transport after connect error', {
|
|
19
|
+
error: getErrorMessage(closeError),
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
logError('Failed to initialize MCP session', error instanceof Error ? error : undefined);
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function createAndConnectTransport({ options, res, }) {
|
|
27
|
+
if (!reserveSessionIfPossible({ options, res }))
|
|
28
|
+
return null;
|
|
29
|
+
const tracker = createSlotTracker();
|
|
30
|
+
const timeoutController = createTimeoutController();
|
|
31
|
+
const transport = createSessionTransport({ tracker, timeoutController });
|
|
32
|
+
await connectTransportOrThrow({
|
|
33
|
+
transport,
|
|
34
|
+
clearInitTimeout: timeoutController.clear,
|
|
35
|
+
releaseSlot: tracker.releaseSlot,
|
|
36
|
+
});
|
|
37
|
+
const sessionId = resolveSessionId({
|
|
38
|
+
transport,
|
|
39
|
+
res,
|
|
40
|
+
tracker,
|
|
41
|
+
clearInitTimeout: timeoutController.clear,
|
|
42
|
+
});
|
|
43
|
+
if (!sessionId)
|
|
44
|
+
return null;
|
|
45
|
+
finalizeSession({
|
|
46
|
+
store: options.sessionStore,
|
|
47
|
+
transport,
|
|
48
|
+
sessionId,
|
|
49
|
+
tracker,
|
|
50
|
+
clearInitTimeout: timeoutController.clear,
|
|
51
|
+
});
|
|
52
|
+
return transport;
|
|
53
|
+
}
|
|
54
|
+
function reserveSessionIfPossible({ options, res, }) {
|
|
55
|
+
if (!ensureSessionCapacity({
|
|
56
|
+
store: options.sessionStore,
|
|
57
|
+
maxSessions: options.maxSessions,
|
|
58
|
+
res,
|
|
59
|
+
evictOldest: evictOldestSession,
|
|
60
|
+
})) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
if (!reserveSessionSlot(options.sessionStore, options.maxSessions)) {
|
|
64
|
+
respondServerBusy(res);
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
function resolveSessionId({ transport, res, tracker, clearInitTimeout, }) {
|
|
70
|
+
const { sessionId } = transport;
|
|
71
|
+
if (typeof sessionId !== 'string') {
|
|
72
|
+
clearInitTimeout();
|
|
73
|
+
tracker.releaseSlot();
|
|
74
|
+
respondBadRequest(res);
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return sessionId;
|
|
78
|
+
}
|
|
79
|
+
function finalizeSession({ store, transport, sessionId, tracker, clearInitTimeout, }) {
|
|
80
|
+
clearInitTimeout();
|
|
81
|
+
tracker.markInitialized();
|
|
82
|
+
tracker.releaseSlot();
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
store.set(sessionId, {
|
|
85
|
+
transport,
|
|
86
|
+
createdAt: now,
|
|
87
|
+
lastSeen: now,
|
|
88
|
+
});
|
|
89
|
+
transport.onclose = () => {
|
|
90
|
+
store.remove(sessionId);
|
|
91
|
+
logInfo('Session closed');
|
|
92
|
+
};
|
|
93
|
+
logInfo('Session initialized');
|
|
94
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Response } from 'express';
|
|
2
|
+
import type { SessionStore } from './sessions.js';
|
|
3
|
+
export interface SlotTracker {
|
|
4
|
+
readonly releaseSlot: () => void;
|
|
5
|
+
readonly markInitialized: () => void;
|
|
6
|
+
readonly isInitialized: () => boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function reserveSessionSlot(store: SessionStore, maxSessions: number): boolean;
|
|
9
|
+
export declare function createSlotTracker(): SlotTracker;
|
|
10
|
+
export declare function ensureSessionCapacity({ store, maxSessions, res, evictOldest, }: {
|
|
11
|
+
store: SessionStore;
|
|
12
|
+
maxSessions: number;
|
|
13
|
+
res: Response;
|
|
14
|
+
evictOldest: (store: SessionStore) => boolean;
|
|
15
|
+
}): boolean;
|
|
16
|
+
export declare function respondServerBusy(res: Response): void;
|
|
17
|
+
export declare function respondBadRequest(res: Response): void;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { sendJsonRpcError } from './jsonrpc-http.js';
|
|
2
|
+
let inFlightSessions = 0;
|
|
3
|
+
export function reserveSessionSlot(store, maxSessions) {
|
|
4
|
+
if (store.size() + inFlightSessions >= maxSessions) {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
inFlightSessions += 1;
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
function releaseSessionSlot() {
|
|
11
|
+
if (inFlightSessions > 0) {
|
|
12
|
+
inFlightSessions -= 1;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function createSlotTracker() {
|
|
16
|
+
let slotReleased = false;
|
|
17
|
+
let initialized = false;
|
|
18
|
+
return {
|
|
19
|
+
releaseSlot: () => {
|
|
20
|
+
if (slotReleased)
|
|
21
|
+
return;
|
|
22
|
+
slotReleased = true;
|
|
23
|
+
releaseSessionSlot();
|
|
24
|
+
},
|
|
25
|
+
markInitialized: () => {
|
|
26
|
+
initialized = true;
|
|
27
|
+
},
|
|
28
|
+
isInitialized: () => initialized,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function ensureSessionCapacity({ store, maxSessions, res, evictOldest, }) {
|
|
32
|
+
if (!isServerAtCapacity(store, maxSessions)) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
if (tryEvictSlot(store, maxSessions, evictOldest)) {
|
|
36
|
+
return !isServerAtCapacity(store, maxSessions);
|
|
37
|
+
}
|
|
38
|
+
respondServerBusy(res);
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
function isServerAtCapacity(store, maxSessions) {
|
|
42
|
+
return store.size() + inFlightSessions >= maxSessions;
|
|
43
|
+
}
|
|
44
|
+
function tryEvictSlot(store, maxSessions, evictOldest) {
|
|
45
|
+
const currentSize = store.size();
|
|
46
|
+
const canFreeSlot = currentSize >= maxSessions &&
|
|
47
|
+
currentSize - 1 + inFlightSessions < maxSessions;
|
|
48
|
+
return canFreeSlot && evictOldest(store);
|
|
49
|
+
}
|
|
50
|
+
export function respondServerBusy(res) {
|
|
51
|
+
sendJsonRpcError(res, -32000, 'Server busy: maximum sessions reached', 503);
|
|
52
|
+
}
|
|
53
|
+
export function respondBadRequest(res) {
|
|
54
|
+
sendJsonRpcError(res, -32000, 'Bad Request: Missing session ID or not an initialize request', 400);
|
|
55
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
2
|
+
import type { SlotTracker } from './mcp-session-slots.js';
|
|
3
|
+
import type { createTimeoutController } from './mcp-session-transport.js';
|
|
4
|
+
export declare function createSessionTransport({ tracker, timeoutController, }: {
|
|
5
|
+
tracker: SlotTracker;
|
|
6
|
+
timeoutController: ReturnType<typeof createTimeoutController>;
|
|
7
|
+
}): StreamableHTTPServerTransport;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
3
|
+
import { config } from '../config/index.js';
|
|
4
|
+
import { logWarn } from '../services/logger.js';
|
|
5
|
+
import { getErrorMessage } from '../utils/error-details.js';
|
|
6
|
+
function startSessionInitTimeout({ transport, tracker, clearInitTimeout, timeoutMs, }) {
|
|
7
|
+
if (timeoutMs <= 0)
|
|
8
|
+
return null;
|
|
9
|
+
const timeout = setTimeout(() => {
|
|
10
|
+
clearInitTimeout();
|
|
11
|
+
if (tracker.isInitialized())
|
|
12
|
+
return;
|
|
13
|
+
tracker.releaseSlot();
|
|
14
|
+
void transport.close().catch((error) => {
|
|
15
|
+
logWarn('Failed to close stalled session', {
|
|
16
|
+
error: getErrorMessage(error),
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
logWarn('Session initialization timed out', { timeoutMs });
|
|
20
|
+
}, timeoutMs);
|
|
21
|
+
timeout.unref();
|
|
22
|
+
return timeout;
|
|
23
|
+
}
|
|
24
|
+
export function createSessionTransport({ tracker, timeoutController, }) {
|
|
25
|
+
const transport = new StreamableHTTPServerTransport({
|
|
26
|
+
sessionIdGenerator: () => randomUUID(),
|
|
27
|
+
});
|
|
28
|
+
transport.onclose = () => {
|
|
29
|
+
timeoutController.clear();
|
|
30
|
+
if (!tracker.isInitialized()) {
|
|
31
|
+
tracker.releaseSlot();
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
timeoutController.set(startSessionInitTimeout({
|
|
35
|
+
transport,
|
|
36
|
+
tracker,
|
|
37
|
+
clearInitTimeout: timeoutController.clear,
|
|
38
|
+
timeoutMs: config.server.sessionInitTimeoutMs,
|
|
39
|
+
}));
|
|
40
|
+
return transport;
|
|
41
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
2
|
+
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
3
|
+
export declare function createTimeoutController(): {
|
|
4
|
+
clear: () => void;
|
|
5
|
+
set: (timeout: NodeJS.Timeout | null) => void;
|
|
6
|
+
};
|
|
7
|
+
export declare function createTransportAdapter(transport: StreamableHTTPServerTransport): Transport;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export function createTimeoutController() {
|
|
2
|
+
let initTimeout = null;
|
|
3
|
+
return {
|
|
4
|
+
clear: () => {
|
|
5
|
+
if (!initTimeout)
|
|
6
|
+
return;
|
|
7
|
+
clearTimeout(initTimeout);
|
|
8
|
+
initTimeout = null;
|
|
9
|
+
},
|
|
10
|
+
set: (timeout) => {
|
|
11
|
+
initTimeout = timeout;
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function createTransportAdapter(transport) {
|
|
16
|
+
const adapter = buildTransportAdapter(transport);
|
|
17
|
+
attachTransportAccessors(adapter, transport);
|
|
18
|
+
return adapter;
|
|
19
|
+
}
|
|
20
|
+
function buildTransportAdapter(transport) {
|
|
21
|
+
return {
|
|
22
|
+
start: () => transport.start(),
|
|
23
|
+
send: (message, options) => transport.send(message, options),
|
|
24
|
+
close: () => transport.close(),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function createAccessorDescriptor(getter, setter) {
|
|
28
|
+
return {
|
|
29
|
+
get: getter,
|
|
30
|
+
...(setter ? { set: setter } : {}),
|
|
31
|
+
enumerable: true,
|
|
32
|
+
configurable: true,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function createOnCloseDescriptor(transport) {
|
|
36
|
+
return createAccessorDescriptor(() => transport.onclose, (handler) => {
|
|
37
|
+
transport.onclose = handler;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function createOnErrorDescriptor(transport) {
|
|
41
|
+
return createAccessorDescriptor(() => transport.onerror, (handler) => {
|
|
42
|
+
transport.onerror = handler;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function createOnMessageDescriptor(transport) {
|
|
46
|
+
return createAccessorDescriptor(() => transport.onmessage, (handler) => {
|
|
47
|
+
transport.onmessage = handler;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function attachTransportAccessors(adapter, transport) {
|
|
51
|
+
Object.defineProperties(adapter, {
|
|
52
|
+
onclose: createOnCloseDescriptor(transport),
|
|
53
|
+
onerror: createOnErrorDescriptor(transport),
|
|
54
|
+
onmessage: createOnMessageDescriptor(transport),
|
|
55
|
+
sessionId: createAccessorDescriptor(() => transport.sessionId),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
1
|
+
import type { Response } from 'express';
|
|
2
|
+
import type { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
3
3
|
import type { McpRequestBody } from '../config/types/runtime.js';
|
|
4
|
-
import {
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
import type { McpSessionOptions } from './mcp-session-types.js';
|
|
5
|
+
export declare function resolveTransportForPost({ res, body, sessionId, options, }: {
|
|
6
|
+
res: Response;
|
|
7
|
+
body: McpRequestBody;
|
|
8
|
+
sessionId: string | undefined;
|
|
9
|
+
options: McpSessionOptions;
|
|
10
|
+
}): Promise<StreamableHTTPServerTransport | null>;
|