@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.
Files changed (173) hide show
  1. package/README.md +131 -156
  2. package/dist/config/auth-config.d.ts +16 -0
  3. package/dist/config/auth-config.js +53 -0
  4. package/dist/config/constants.d.ts +11 -13
  5. package/dist/config/constants.js +1 -3
  6. package/dist/config/env-parsers.d.ts +7 -0
  7. package/dist/config/env-parsers.js +84 -0
  8. package/dist/config/formatting.d.ts +2 -2
  9. package/dist/config/index.d.ts +47 -53
  10. package/dist/config/index.js +35 -64
  11. package/dist/config/types/content.d.ts +1 -49
  12. package/dist/config/types/runtime.d.ts +8 -16
  13. package/dist/config/types/tools.d.ts +2 -28
  14. package/dist/http/accept-policy.d.ts +3 -0
  15. package/dist/http/accept-policy.js +45 -0
  16. package/dist/http/async-handler.d.ts +2 -0
  17. package/dist/http/async-handler.js +5 -0
  18. package/dist/http/auth-introspection.d.ts +2 -0
  19. package/dist/http/auth-introspection.js +141 -0
  20. package/dist/http/auth-static.d.ts +2 -0
  21. package/dist/http/auth-static.js +23 -0
  22. package/dist/http/auth.d.ts +3 -2
  23. package/dist/http/auth.js +254 -23
  24. package/dist/http/cors.d.ts +6 -6
  25. package/dist/http/cors.js +7 -42
  26. package/dist/http/download-routes.d.ts +0 -12
  27. package/dist/http/download-routes.js +21 -58
  28. package/dist/http/host-allowlist.d.ts +3 -0
  29. package/dist/http/host-allowlist.js +117 -0
  30. package/dist/http/jsonrpc-http.d.ts +2 -0
  31. package/dist/http/jsonrpc-http.js +10 -0
  32. package/dist/http/mcp-routes.d.ts +8 -3
  33. package/dist/http/mcp-routes.js +137 -31
  34. package/dist/http/mcp-session-eviction.d.ts +3 -0
  35. package/dist/http/mcp-session-eviction.js +24 -0
  36. package/dist/http/mcp-session-helpers.d.ts +0 -1
  37. package/dist/http/mcp-session-helpers.js +1 -1
  38. package/dist/http/mcp-session-init.d.ts +7 -0
  39. package/dist/http/mcp-session-init.js +94 -0
  40. package/dist/http/mcp-session-slots.d.ts +17 -0
  41. package/dist/http/mcp-session-slots.js +55 -0
  42. package/dist/http/mcp-session-transport-init.d.ts +7 -0
  43. package/dist/http/mcp-session-transport-init.js +41 -0
  44. package/dist/http/mcp-session-transport.d.ts +7 -0
  45. package/dist/http/mcp-session-transport.js +57 -0
  46. package/dist/http/mcp-session-types.d.ts +5 -0
  47. package/dist/http/mcp-session-types.js +1 -0
  48. package/dist/http/mcp-session.d.ts +9 -9
  49. package/dist/http/mcp-session.js +15 -137
  50. package/dist/http/mcp-sessions.d.ts +43 -0
  51. package/dist/http/mcp-sessions.js +392 -0
  52. package/dist/http/mcp-validation.d.ts +1 -0
  53. package/dist/http/mcp-validation.js +11 -10
  54. package/dist/http/protocol-policy.d.ts +2 -0
  55. package/dist/http/protocol-policy.js +31 -0
  56. package/dist/http/rate-limit.js +7 -4
  57. package/dist/http/server-config.d.ts +1 -0
  58. package/dist/http/server-config.js +40 -0
  59. package/dist/http/server-middleware.d.ts +7 -9
  60. package/dist/http/server-middleware.js +9 -70
  61. package/dist/http/server-shutdown.d.ts +4 -0
  62. package/dist/http/server-shutdown.js +43 -0
  63. package/dist/http/server.d.ts +10 -0
  64. package/dist/http/server.js +546 -61
  65. package/dist/http/session-cleanup.js +8 -5
  66. package/dist/middleware/error-handler.d.ts +1 -1
  67. package/dist/middleware/error-handler.js +32 -33
  68. package/dist/resources/cached-content-params.d.ts +5 -0
  69. package/dist/resources/cached-content-params.js +36 -0
  70. package/dist/resources/cached-content.js +67 -125
  71. package/dist/resources/index.js +0 -82
  72. package/dist/server.js +50 -29
  73. package/dist/services/cache-events.d.ts +8 -0
  74. package/dist/services/cache-events.js +19 -0
  75. package/dist/services/cache-keys.d.ts +7 -0
  76. package/dist/services/cache-keys.js +57 -0
  77. package/dist/services/cache.d.ts +4 -9
  78. package/dist/services/cache.js +77 -139
  79. package/dist/services/context.d.ts +0 -1
  80. package/dist/services/context.js +0 -7
  81. package/dist/services/extractor.js +55 -116
  82. package/dist/services/fetcher/agents.d.ts +2 -2
  83. package/dist/services/fetcher/agents.js +35 -96
  84. package/dist/services/fetcher/dns-selection.d.ts +2 -0
  85. package/dist/services/fetcher/dns-selection.js +72 -0
  86. package/dist/services/fetcher/interceptors.d.ts +0 -22
  87. package/dist/services/fetcher/interceptors.js +18 -32
  88. package/dist/services/fetcher/redirects.js +16 -7
  89. package/dist/services/fetcher/response.js +79 -34
  90. package/dist/services/fetcher.d.ts +22 -3
  91. package/dist/services/fetcher.js +544 -44
  92. package/dist/services/fifo-queue.d.ts +8 -0
  93. package/dist/services/fifo-queue.js +25 -0
  94. package/dist/services/logger.js +2 -2
  95. package/dist/services/metadata-collector.d.ts +1 -9
  96. package/dist/services/metadata-collector.js +71 -2
  97. package/dist/services/transform-worker-pool.d.ts +4 -14
  98. package/dist/services/transform-worker-pool.js +177 -129
  99. package/dist/services/transform-worker-types.d.ts +32 -0
  100. package/dist/services/transform-worker-types.js +14 -0
  101. package/dist/tools/handlers/fetch-markdown.tool.d.ts +3 -4
  102. package/dist/tools/handlers/fetch-markdown.tool.js +20 -72
  103. package/dist/tools/handlers/fetch-single.shared.d.ts +11 -22
  104. package/dist/tools/handlers/fetch-single.shared.js +175 -89
  105. package/dist/tools/handlers/fetch-url.tool.d.ts +7 -1
  106. package/dist/tools/handlers/fetch-url.tool.js +84 -119
  107. package/dist/tools/index.js +21 -40
  108. package/dist/tools/schemas.d.ts +1 -51
  109. package/dist/tools/schemas.js +1 -107
  110. package/dist/tools/utils/cached-markdown.d.ts +5 -0
  111. package/dist/tools/utils/cached-markdown.js +46 -0
  112. package/dist/tools/utils/content-shaping.d.ts +4 -0
  113. package/dist/tools/utils/content-shaping.js +67 -0
  114. package/dist/tools/utils/content-transform.d.ts +5 -17
  115. package/dist/tools/utils/content-transform.js +134 -114
  116. package/dist/tools/utils/fetch-pipeline.d.ts +0 -8
  117. package/dist/tools/utils/fetch-pipeline.js +57 -63
  118. package/dist/tools/utils/frontmatter.d.ts +3 -0
  119. package/dist/tools/utils/frontmatter.js +73 -0
  120. package/dist/tools/utils/inline-content.d.ts +1 -2
  121. package/dist/tools/utils/inline-content.js +4 -7
  122. package/dist/tools/utils/markdown-heuristics.d.ts +1 -0
  123. package/dist/tools/utils/markdown-heuristics.js +19 -0
  124. package/dist/tools/utils/markdown-signals.d.ts +1 -0
  125. package/dist/tools/utils/markdown-signals.js +19 -0
  126. package/dist/tools/utils/raw-markdown-frontmatter.d.ts +3 -0
  127. package/dist/tools/utils/raw-markdown-frontmatter.js +73 -0
  128. package/dist/tools/utils/raw-markdown.d.ts +6 -0
  129. package/dist/tools/utils/raw-markdown.js +135 -0
  130. package/dist/transformers/markdown/fenced-code-rule.d.ts +2 -0
  131. package/dist/transformers/markdown/fenced-code-rule.js +38 -0
  132. package/dist/transformers/markdown/frontmatter.d.ts +2 -0
  133. package/dist/transformers/markdown/frontmatter.js +45 -0
  134. package/dist/transformers/markdown/noise-rule.d.ts +2 -0
  135. package/dist/transformers/markdown/noise-rule.js +80 -0
  136. package/dist/transformers/markdown/turndown-instance.d.ts +2 -0
  137. package/dist/transformers/markdown/turndown-instance.js +19 -0
  138. package/dist/transformers/markdown.d.ts +2 -0
  139. package/dist/transformers/markdown.js +185 -0
  140. package/dist/transformers/markdown.transformer.js +5 -117
  141. package/dist/utils/cached-payload.d.ts +7 -0
  142. package/dist/utils/cached-payload.js +36 -0
  143. package/dist/utils/code-language-bash.d.ts +1 -0
  144. package/dist/utils/code-language-bash.js +48 -0
  145. package/dist/utils/code-language-core.d.ts +2 -0
  146. package/dist/utils/code-language-core.js +13 -0
  147. package/dist/utils/code-language-detectors.d.ts +5 -0
  148. package/dist/utils/code-language-detectors.js +142 -0
  149. package/dist/utils/code-language-helpers.d.ts +5 -0
  150. package/dist/utils/code-language-helpers.js +62 -0
  151. package/dist/utils/code-language-parsing.d.ts +5 -0
  152. package/dist/utils/code-language-parsing.js +62 -0
  153. package/dist/utils/code-language.d.ts +9 -0
  154. package/dist/utils/code-language.js +250 -46
  155. package/dist/utils/error-details.d.ts +3 -0
  156. package/dist/utils/error-details.js +12 -0
  157. package/dist/utils/error-utils.js +1 -1
  158. package/dist/utils/filename-generator.js +34 -12
  159. package/dist/utils/guards.d.ts +1 -0
  160. package/dist/utils/guards.js +3 -0
  161. package/dist/utils/header-normalizer.d.ts +0 -3
  162. package/dist/utils/header-normalizer.js +3 -3
  163. package/dist/utils/ip-address.d.ts +4 -0
  164. package/dist/utils/ip-address.js +6 -0
  165. package/dist/utils/tool-error-handler.d.ts +2 -2
  166. package/dist/utils/tool-error-handler.js +14 -46
  167. package/dist/utils/url-transformer.d.ts +7 -0
  168. package/dist/utils/url-transformer.js +147 -0
  169. package/dist/utils/url-validator.d.ts +1 -2
  170. package/dist/utils/url-validator.js +53 -114
  171. package/dist/workers/content-transform.worker.d.ts +1 -0
  172. package/dist/workers/content-transform.worker.js +40 -0
  173. package/package.json +17 -18
@@ -1,31 +1,48 @@
1
+ import { z } from 'zod';
1
2
  import { logError, logInfo } from '../services/logger.js';
2
- import { resolveTransportForPost, } from './mcp-session.js';
3
- import { isMcpRequestBody } from './mcp-validation.js';
4
- import { getSessionId } from './sessions.js';
5
- function sendJsonRpcError(res, code, message, status = 400) {
6
- res.status(status).json({
7
- jsonrpc: '2.0',
8
- error: {
9
- code,
10
- message,
11
- },
12
- id: null,
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.status(400).json({ error: 'Missing mcp-session-id header' });
26
+ sendJsonRpcError(res, -32600, 'Missing mcp-session-id header', 400);
20
27
  }
21
28
  function respondSessionNotFound(res) {
22
- res.status(404).json({ error: 'Session not found' });
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 = options.sessionStore.get(sessionId);
75
+ const session = sessionStore.get(sessionId);
58
76
  if (!session) {
59
77
  respondSessionNotFound(res);
60
78
  return null;
61
79
  }
62
- options.sessionStore.touch(sessionId);
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 { body } = req;
68
- if (!isMcpRequestBody(body)) {
69
- respondInvalidRequestBody(res);
163
+ const payload = validatePostPayload(req.body, res);
164
+ if (!payload)
70
165
  return;
71
- }
72
- logPostRequest(body, sessionId, options);
73
- const transport = await resolveTransportForPost(req, res, body, sessionId, options);
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, body);
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
- const asyncHandler = (fn) => (req, res, next) => {
92
- Promise.resolve(fn(req, res)).catch(next);
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,3 @@
1
+ import type { SessionStore } from './sessions.js';
2
+ export declare function evictExpiredSessions(store: SessionStore): number;
3
+ export declare function evictOldestSession(store: SessionStore): boolean;
@@ -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;
@@ -6,7 +6,7 @@ export function reserveSessionSlot(store, maxSessions) {
6
6
  inFlightSessions += 1;
7
7
  return true;
8
8
  }
9
- export function releaseSessionSlot() {
9
+ function releaseSessionSlot() {
10
10
  if (inFlightSessions > 0) {
11
11
  inFlightSessions -= 1;
12
12
  }
@@ -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,5 @@
1
+ import type { SessionStore } from './sessions.js';
2
+ export interface McpSessionOptions {
3
+ readonly sessionStore: SessionStore;
4
+ readonly maxSessions: number;
5
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,10 +1,10 @@
1
- import type { Request, Response } from 'express';
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 { type SessionStore } from './sessions.js';
5
- export interface McpSessionOptions {
6
- readonly sessionStore: SessionStore;
7
- readonly maxSessions: number;
8
- }
9
- export declare function resolveTransportForPost(_req: Request, res: Response, body: McpRequestBody, sessionId: string | undefined, options: McpSessionOptions): Promise<StreamableHTTPServerTransport | null>;
10
- export declare function evictExpiredSessions(store: SessionStore): number;
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>;