@j0hanz/superfetch 1.2.5 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.md +116 -152
  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 +25 -59
  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 +98 -26
  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/jsonrpc-http.d.ts +2 -0
  29. package/dist/http/jsonrpc-http.js +10 -0
  30. package/dist/http/mcp-routes.d.ts +0 -1
  31. package/dist/http/mcp-routes.js +43 -30
  32. package/dist/http/mcp-session-helpers.d.ts +0 -1
  33. package/dist/http/mcp-session-helpers.js +1 -1
  34. package/dist/http/mcp-session-transport.d.ts +7 -0
  35. package/dist/http/mcp-session-transport.js +57 -0
  36. package/dist/http/mcp-session.js +60 -73
  37. package/dist/http/mcp-validation.d.ts +1 -0
  38. package/dist/http/mcp-validation.js +11 -10
  39. package/dist/http/protocol-policy.d.ts +2 -0
  40. package/dist/http/protocol-policy.js +31 -0
  41. package/dist/http/rate-limit.js +5 -2
  42. package/dist/http/server-config.d.ts +1 -0
  43. package/dist/http/server-config.js +40 -0
  44. package/dist/http/server-middleware.d.ts +2 -9
  45. package/dist/http/server-middleware.js +96 -43
  46. package/dist/http/server-shutdown.d.ts +4 -0
  47. package/dist/http/server-shutdown.js +43 -0
  48. package/dist/http/server.js +52 -64
  49. package/dist/http/session-cleanup.js +1 -1
  50. package/dist/middleware/error-handler.js +1 -3
  51. package/dist/resources/cached-content.js +50 -108
  52. package/dist/resources/index.js +0 -82
  53. package/dist/server.js +51 -30
  54. package/dist/services/cache-keys.d.ts +7 -0
  55. package/dist/services/cache-keys.js +57 -0
  56. package/dist/services/cache.d.ts +1 -7
  57. package/dist/services/cache.js +53 -119
  58. package/dist/services/context.d.ts +0 -1
  59. package/dist/services/context.js +0 -7
  60. package/dist/services/extractor.js +10 -82
  61. package/dist/services/fetcher/agents.d.ts +2 -2
  62. package/dist/services/fetcher/agents.js +34 -95
  63. package/dist/services/fetcher/dns-selection.d.ts +2 -0
  64. package/dist/services/fetcher/dns-selection.js +72 -0
  65. package/dist/services/fetcher/interceptors.d.ts +0 -22
  66. package/dist/services/fetcher/interceptors.js +30 -13
  67. package/dist/services/fetcher/redirects.js +4 -3
  68. package/dist/services/fetcher/response.js +66 -31
  69. package/dist/services/fetcher.d.ts +1 -3
  70. package/dist/services/fetcher.js +14 -33
  71. package/dist/services/fifo-queue.d.ts +8 -0
  72. package/dist/services/fifo-queue.js +25 -0
  73. package/dist/services/logger.js +2 -2
  74. package/dist/services/metadata-collector.d.ts +1 -9
  75. package/dist/services/metadata-collector.js +71 -2
  76. package/dist/services/transform-worker-pool.d.ts +4 -14
  77. package/dist/services/transform-worker-pool.js +177 -129
  78. package/dist/services/transform-worker-types.d.ts +32 -0
  79. package/dist/services/transform-worker-types.js +14 -0
  80. package/dist/tools/handlers/fetch-markdown.tool.d.ts +3 -4
  81. package/dist/tools/handlers/fetch-markdown.tool.js +20 -72
  82. package/dist/tools/handlers/fetch-single.shared.d.ts +1 -20
  83. package/dist/tools/handlers/fetch-single.shared.js +44 -87
  84. package/dist/tools/handlers/fetch-url.tool.d.ts +1 -1
  85. package/dist/tools/handlers/fetch-url.tool.js +46 -123
  86. package/dist/tools/index.js +21 -40
  87. package/dist/tools/schemas.d.ts +1 -51
  88. package/dist/tools/schemas.js +1 -107
  89. package/dist/tools/utils/cached-markdown.d.ts +5 -0
  90. package/dist/tools/utils/cached-markdown.js +46 -0
  91. package/dist/tools/utils/content-shaping.d.ts +4 -0
  92. package/dist/tools/utils/content-shaping.js +52 -0
  93. package/dist/tools/utils/content-transform.d.ts +2 -17
  94. package/dist/tools/utils/content-transform.js +120 -114
  95. package/dist/tools/utils/fetch-pipeline.d.ts +0 -8
  96. package/dist/tools/utils/fetch-pipeline.js +65 -62
  97. package/dist/tools/utils/inline-content.d.ts +1 -2
  98. package/dist/tools/utils/inline-content.js +4 -7
  99. package/dist/transformers/markdown.transformer.js +109 -34
  100. package/dist/utils/cached-payload.d.ts +7 -0
  101. package/dist/utils/cached-payload.js +36 -0
  102. package/dist/utils/error-utils.js +1 -1
  103. package/dist/utils/filename-generator.js +21 -10
  104. package/dist/utils/guards.d.ts +1 -0
  105. package/dist/utils/guards.js +3 -0
  106. package/dist/utils/header-normalizer.d.ts +0 -3
  107. package/dist/utils/header-normalizer.js +3 -3
  108. package/dist/utils/tool-error-handler.d.ts +2 -2
  109. package/dist/utils/tool-error-handler.js +11 -38
  110. package/dist/utils/url-transformer.d.ts +7 -0
  111. package/dist/utils/url-transformer.js +147 -0
  112. package/dist/utils/url-validator.d.ts +1 -2
  113. package/dist/utils/url-validator.js +20 -93
  114. package/dist/workers/content-transform.worker.d.ts +1 -0
  115. package/dist/workers/content-transform.worker.js +40 -0
  116. package/package.json +13 -16
package/dist/http/auth.js CHANGED
@@ -1,38 +1,110 @@
1
- import { timingSafeEqualUtf8 } from '../utils/crypto.js';
1
+ import { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js';
2
+ import { getOAuthProtectedResourceMetadataUrl, mcpAuthMetadataRouter, } from '@modelcontextprotocol/sdk/server/auth/router.js';
3
+ import { config } from '../config/index.js';
4
+ import { verifyWithIntrospection } from './auth-introspection.js';
5
+ import { verifyStaticToken } from './auth-static.js';
2
6
  function normalizeHeaderValue(header) {
3
7
  return Array.isArray(header) ? header[0] : header;
4
8
  }
5
- function timingSafeEquals(a, b) {
6
- return timingSafeEqualUtf8(a, b);
7
- }
8
- function isAuthorizedRequest(req, authToken) {
9
- if (!authToken)
10
- return false;
11
- const bearerToken = getBearerToken(req);
12
- if (bearerToken) {
13
- return timingSafeEquals(bearerToken, authToken);
14
- }
15
- const apiKeyHeader = getApiKeyHeader(req);
16
- return apiKeyHeader ? timingSafeEquals(apiKeyHeader, authToken) : false;
17
- }
18
- function getBearerToken(req) {
19
- const authHeader = normalizeHeaderValue(req.headers.authorization);
20
- if (!authHeader?.startsWith('Bearer '))
21
- return null;
22
- const token = authHeader.slice('Bearer '.length).trim();
23
- return token.length > 0 ? token : null;
24
- }
25
9
  function getApiKeyHeader(req) {
26
10
  const apiKeyHeader = normalizeHeaderValue(req.headers['x-api-key']);
27
11
  return apiKeyHeader ? apiKeyHeader.trim() : null;
28
12
  }
29
- export function createAuthMiddleware(authToken) {
30
- return (req, res, next) => {
31
- if (isAuthorizedRequest(req, authToken)) {
13
+ function createLegacyApiKeyMiddleware() {
14
+ return (req, _res, next) => {
15
+ if (config.auth.mode !== 'static') {
32
16
  next();
33
17
  return;
34
18
  }
35
- res.set('WWW-Authenticate', 'Bearer realm="mcp", error="invalid_token", error_description="Missing or invalid credentials"');
36
- res.status(401).json({ error: 'Unauthorized' });
19
+ if (!req.headers.authorization) {
20
+ const apiKey = getApiKeyHeader(req);
21
+ if (apiKey) {
22
+ req.headers.authorization = `Bearer ${apiKey}`;
23
+ }
24
+ }
25
+ next();
37
26
  };
38
27
  }
28
+ async function verifyAccessToken(token) {
29
+ if (config.auth.mode === 'oauth') {
30
+ return verifyWithIntrospection(token);
31
+ }
32
+ return verifyStaticToken(token);
33
+ }
34
+ function resolveMetadataUrl() {
35
+ if (config.auth.mode !== 'oauth')
36
+ return null;
37
+ return getOAuthProtectedResourceMetadataUrl(new URL(config.auth.resourceUrl));
38
+ }
39
+ function resolveOptionalScopes(requiredScopes) {
40
+ return requiredScopes.length > 0 ? [...requiredScopes] : undefined;
41
+ }
42
+ function resolveOAuthMetadataParams(authConfig) {
43
+ const { issuerUrl, authorizationUrl, tokenUrl, revocationUrl, registrationUrl, requiredScopes, } = authConfig;
44
+ if (!issuerUrl || !authorizationUrl || !tokenUrl)
45
+ return null;
46
+ return {
47
+ issuerUrl,
48
+ authorizationUrl,
49
+ tokenUrl,
50
+ revocationUrl,
51
+ registrationUrl,
52
+ requiredScopes,
53
+ };
54
+ }
55
+ function buildBaseOAuthMetadata(params) {
56
+ return {
57
+ issuer: params.issuerUrl.href,
58
+ authorization_endpoint: params.authorizationUrl.href,
59
+ response_types_supported: ['code'],
60
+ code_challenge_methods_supported: ['S256'],
61
+ token_endpoint: params.tokenUrl.href,
62
+ token_endpoint_auth_methods_supported: ['client_secret_post', 'none'],
63
+ grant_types_supported: ['authorization_code', 'refresh_token'],
64
+ };
65
+ }
66
+ function applyOptionalScopes(metadata, requiredScopes) {
67
+ const scopesSupported = resolveOptionalScopes(requiredScopes);
68
+ if (scopesSupported !== undefined) {
69
+ metadata.scopes_supported = scopesSupported;
70
+ }
71
+ }
72
+ function applyOptionalEndpoint(metadata, key, url) {
73
+ if (!url)
74
+ return;
75
+ metadata[key] = url.href;
76
+ }
77
+ function buildOAuthMetadata(params) {
78
+ const oauthMetadata = buildBaseOAuthMetadata(params);
79
+ applyOptionalScopes(oauthMetadata, params.requiredScopes);
80
+ applyOptionalEndpoint(oauthMetadata, 'revocation_endpoint', params.revocationUrl);
81
+ applyOptionalEndpoint(oauthMetadata, 'registration_endpoint', params.registrationUrl);
82
+ return oauthMetadata;
83
+ }
84
+ export function createAuthMiddleware() {
85
+ const metadataUrl = resolveMetadataUrl();
86
+ const authHandler = requireBearerAuth({
87
+ verifier: { verifyAccessToken },
88
+ requiredScopes: config.auth.requiredScopes,
89
+ ...(metadataUrl ? { resourceMetadataUrl: metadataUrl } : {}),
90
+ });
91
+ const legacyHandler = createLegacyApiKeyMiddleware();
92
+ return (req, res, next) => {
93
+ legacyHandler(req, res, () => {
94
+ authHandler(req, res, next);
95
+ });
96
+ };
97
+ }
98
+ export function createAuthMetadataRouter() {
99
+ if (config.auth.mode !== 'oauth')
100
+ return null;
101
+ const oauthMetadataParams = resolveOAuthMetadataParams(config.auth);
102
+ if (!oauthMetadataParams)
103
+ return null;
104
+ return mcpAuthMetadataRouter({
105
+ oauthMetadata: buildOAuthMetadata(oauthMetadataParams),
106
+ resourceServerUrl: config.auth.resourceUrl,
107
+ scopesSupported: config.auth.requiredScopes,
108
+ resourceName: config.server.name,
109
+ });
110
+ }
@@ -1,7 +1,7 @@
1
1
  import type { NextFunction, Request, Response } from 'express';
2
- interface CorsOptions {
3
- readonly allowedOrigins: string[];
4
- readonly allowAllOrigins: boolean;
5
- }
6
- export declare function createCorsMiddleware(options: CorsOptions): (req: Request, res: Response, next: NextFunction) => void;
7
- export {};
2
+ /**
3
+ * Creates a minimal CORS middleware.
4
+ * MCP clients are not browser-based, so CORS is not needed.
5
+ * This just handles OPTIONS preflight requests.
6
+ */
7
+ export declare function createCorsMiddleware(): (req: Request, res: Response, next: NextFunction) => void;
package/dist/http/cors.js CHANGED
@@ -1,35 +1,11 @@
1
- function isOriginAllowed(origin, options) {
2
- if (!origin)
3
- return true;
4
- if (options.allowAllOrigins)
5
- return true;
6
- if (options.allowedOrigins.length === 0)
7
- return false;
8
- return options.allowedOrigins.includes(origin);
9
- }
10
- function isValidOrigin(origin) {
11
- return URL.canParse(origin);
12
- }
13
- export function createCorsMiddleware(options) {
1
+ /**
2
+ * Creates a minimal CORS middleware.
3
+ * MCP clients are not browser-based, so CORS is not needed.
4
+ * This just handles OPTIONS preflight requests.
5
+ */
6
+ export function createCorsMiddleware() {
14
7
  return (req, res, next) => {
15
- const origin = resolveOrigin(req);
16
- if (origin) {
17
- if (!isValidOrigin(origin)) {
18
- res.status(403).json({
19
- error: 'Origin not allowed',
20
- code: 'ORIGIN_NOT_ALLOWED',
21
- });
22
- return;
23
- }
24
- if (!isOriginAllowed(origin, options)) {
25
- res.status(403).json({
26
- error: 'Origin not allowed',
27
- code: 'ORIGIN_NOT_ALLOWED',
28
- });
29
- return;
30
- }
31
- applyCorsHeaders(res, origin);
32
- }
8
+ // Handle OPTIONS preflight
33
9
  if (req.method === 'OPTIONS') {
34
10
  res.sendStatus(200);
35
11
  return;
@@ -37,14 +13,3 @@ export function createCorsMiddleware(options) {
37
13
  next();
38
14
  };
39
15
  }
40
- function resolveOrigin(req) {
41
- return req.headers.origin;
42
- }
43
- function applyCorsHeaders(res, origin) {
44
- res.vary('Origin');
45
- res.header('Access-Control-Allow-Origin', origin);
46
- res.header('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
47
- res.header('Access-Control-Allow-Headers', 'Content-Type, mcp-session-id, Authorization, X-API-Key');
48
- res.header('Access-Control-Expose-Headers', 'mcp-session-id');
49
- res.header('Access-Control-Max-Age', '86400');
50
- }
@@ -1,14 +1,2 @@
1
1
  import type { Express } from 'express';
2
- import type { CacheEntry } from '../config/types/content.js';
3
- interface DownloadParams {
4
- namespace: string;
5
- hash: string;
6
- }
7
- interface DownloadPayload {
8
- content: string;
9
- contentType: string;
10
- fileName: string;
11
- }
12
- export declare function resolveDownloadPayload(params: DownloadParams, cacheEntry: CacheEntry): DownloadPayload | null;
13
2
  export declare function registerDownloadRoutes(app: Express): void;
14
- export {};
@@ -1,11 +1,12 @@
1
1
  import { config } from '../config/index.js';
2
2
  import * as cache from '../services/cache.js';
3
3
  import { logDebug } from '../services/logger.js';
4
+ import { parseCachedPayload, resolveCachedPayloadContent, } from '../utils/cached-payload.js';
4
5
  import { generateSafeFilename } from '../utils/filename-generator.js';
5
- const VALID_NAMESPACES = new Set(['markdown', 'url']);
6
+ import { wrapAsync } from './async-handler.js';
6
7
  const HASH_PATTERN = /^[a-f0-9.]+$/i;
7
8
  function validateNamespace(namespace) {
8
- return VALID_NAMESPACES.has(namespace);
9
+ return namespace === 'markdown';
9
10
  }
10
11
  function validateHash(hash) {
11
12
  return HASH_PATTERN.test(hash) && hash.length >= 8 && hash.length <= 64;
@@ -41,55 +42,18 @@ function respondServiceUnavailable(res) {
41
42
  code: 'SERVICE_UNAVAILABLE',
42
43
  });
43
44
  }
44
- function resolveContentType(namespace) {
45
- return namespace === 'markdown'
46
- ? 'text/markdown; charset=utf-8'
47
- : 'application/x-ndjson; charset=utf-8';
48
- }
49
- function resolveExtension(namespace) {
50
- return namespace === 'markdown' ? '.md' : '.jsonl';
51
- }
52
- function parseCachedPayload(raw) {
53
- try {
54
- const parsed = JSON.parse(raw);
55
- return isCachedPayload(parsed) ? parsed : null;
56
- }
57
- catch {
58
- return null;
59
- }
60
- }
61
- function isCachedPayload(value) {
62
- if (!value || typeof value !== 'object')
63
- return false;
64
- const record = value;
65
- return ((record.content === undefined || typeof record.content === 'string') &&
66
- (record.markdown === undefined || typeof record.markdown === 'string') &&
67
- (record.title === undefined || typeof record.title === 'string'));
68
- }
69
- function resolvePayloadContent(payload, namespace) {
70
- if (namespace === 'markdown') {
71
- if (typeof payload.markdown === 'string') {
72
- return payload.markdown;
73
- }
74
- if (typeof payload.content === 'string') {
75
- return payload.content;
76
- }
77
- return null;
78
- }
79
- return typeof payload.content === 'string' ? payload.content : null;
80
- }
81
- export function resolveDownloadPayload(params, cacheEntry) {
45
+ function resolveDownloadPayload(params, cacheEntry) {
82
46
  const payload = parseCachedPayload(cacheEntry.content);
83
47
  if (!payload)
84
48
  return null;
85
- const content = resolvePayloadContent(payload, params.namespace);
49
+ const content = resolveCachedPayloadContent(payload);
86
50
  if (!content)
87
51
  return null;
88
52
  const safeTitle = typeof payload.title === 'string' ? payload.title : undefined;
89
- const fileName = generateSafeFilename(cacheEntry.url, cacheEntry.title ?? safeTitle, params.hash, resolveExtension(params.namespace));
53
+ const fileName = generateSafeFilename(cacheEntry.url, cacheEntry.title ?? safeTitle, params.hash, '.md');
90
54
  return {
91
55
  content,
92
- contentType: resolveContentType(params.namespace),
56
+ contentType: 'text/markdown; charset=utf-8',
93
57
  fileName,
94
58
  };
95
59
  }
@@ -97,41 +61,40 @@ function buildContentDisposition(fileName) {
97
61
  const encodedName = encodeURIComponent(fileName).replace(/'/g, '%27');
98
62
  return `attachment; filename="${fileName}"; filename*=UTF-8''${encodedName}`;
99
63
  }
64
+ function sendDownloadPayload(res, payload) {
65
+ const disposition = buildContentDisposition(payload.fileName);
66
+ res.setHeader('Content-Type', payload.contentType);
67
+ res.setHeader('Content-Disposition', disposition);
68
+ res.setHeader('Cache-Control', `private, max-age=${config.cache.ttl}`);
69
+ res.setHeader('X-Content-Type-Options', 'nosniff');
70
+ res.send(payload.content);
71
+ }
100
72
  function handleDownload(req, res) {
101
73
  if (!config.cache.enabled) {
102
74
  respondServiceUnavailable(res);
103
- return Promise.resolve();
75
+ return;
104
76
  }
105
77
  const params = parseDownloadParams(req);
106
78
  if (!params) {
107
79
  respondBadRequest(res, 'Invalid namespace or hash format');
108
- return Promise.resolve();
80
+ return;
109
81
  }
110
82
  const cacheKey = buildCacheKeyFromParams(params);
111
83
  const cacheEntry = cache.get(cacheKey);
112
84
  if (!cacheEntry) {
113
85
  logDebug('Download request for missing cache key', { cacheKey });
114
86
  respondNotFound(res);
115
- return Promise.resolve();
87
+ return;
116
88
  }
117
89
  const payload = resolveDownloadPayload(params, cacheEntry);
118
90
  if (!payload) {
119
91
  logDebug('Download payload unavailable', { cacheKey });
120
92
  respondNotFound(res);
121
- return Promise.resolve();
93
+ return;
122
94
  }
123
- const disposition = buildContentDisposition(payload.fileName);
124
- res.setHeader('Content-Type', payload.contentType);
125
- res.setHeader('Content-Disposition', disposition);
126
- res.setHeader('Cache-Control', `private, max-age=${config.cache.ttl}`);
127
- res.setHeader('X-Content-Type-Options', 'nosniff');
128
95
  logDebug('Serving download', { cacheKey, fileName: payload.fileName });
129
- res.send(payload.content);
130
- return Promise.resolve();
96
+ sendDownloadPayload(res, payload);
131
97
  }
132
98
  export function registerDownloadRoutes(app) {
133
- const asyncHandler = (fn) => (req, res, next) => {
134
- Promise.resolve(fn(req, res)).catch(next);
135
- };
136
- app.get('/mcp/downloads/:namespace/:hash', asyncHandler(handleDownload));
99
+ app.get('/mcp/downloads/:namespace/:hash', wrapAsync(handleDownload));
137
100
  }
@@ -0,0 +1,2 @@
1
+ import type { Response } from 'express';
2
+ export declare function sendJsonRpcError(res: Response, code: number, message: string, status?: number): void;
@@ -0,0 +1,10 @@
1
+ export function sendJsonRpcError(res, code, message, status = 400) {
2
+ res.status(status).json({
3
+ jsonrpc: '2.0',
4
+ error: {
5
+ code,
6
+ message,
7
+ },
8
+ id: null,
9
+ });
10
+ }
@@ -1,4 +1,3 @@
1
1
  import type { Express } from 'express';
2
2
  import { type McpSessionOptions } from './mcp-session.js';
3
3
  export declare function registerMcpRoutes(app: Express, options: McpSessionOptions): void;
4
- export { evictExpiredSessions } from './mcp-session.js';
@@ -1,31 +1,35 @@
1
1
  import { logError, logInfo } from '../services/logger.js';
2
+ import { acceptsEventStream, ensurePostAcceptHeader } from './accept-policy.js';
3
+ import { wrapAsync } from './async-handler.js';
4
+ import { sendJsonRpcError } from './jsonrpc-http.js';
2
5
  import { resolveTransportForPost, } from './mcp-session.js';
3
- import { isMcpRequestBody } from './mcp-validation.js';
6
+ import { isJsonRpcBatchRequest, isMcpRequestBody } from './mcp-validation.js';
7
+ import { ensureMcpProtocolVersionHeader } from './protocol-policy.js';
4
8
  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
- });
14
- }
15
9
  function respondInvalidRequestBody(res) {
16
10
  sendJsonRpcError(res, -32600, 'Invalid Request: Malformed request body', 400);
17
11
  }
18
12
  function respondMissingSession(res) {
19
- res.status(400).json({ error: 'Missing mcp-session-id header' });
13
+ sendJsonRpcError(res, -32600, 'Missing mcp-session-id header', 400);
20
14
  }
21
15
  function respondSessionNotFound(res) {
22
- res.status(404).json({ error: 'Session not found' });
16
+ sendJsonRpcError(res, -32600, 'Session not found', 404);
17
+ }
18
+ function validatePostPayload(payload, res) {
19
+ if (isJsonRpcBatchRequest(payload)) {
20
+ sendJsonRpcError(res, -32600, 'Batch requests are not supported', 400);
21
+ return null;
22
+ }
23
+ if (!isMcpRequestBody(payload)) {
24
+ respondInvalidRequestBody(res);
25
+ return null;
26
+ }
27
+ return payload;
23
28
  }
24
29
  function logPostRequest(body, sessionId, options) {
25
30
  logInfo('[MCP POST]', {
26
31
  method: body.method,
27
32
  id: body.id,
28
- sessionId: sessionId ?? 'none',
29
33
  isInitialize: body.method === 'initialize',
30
34
  sessionCount: options.sessionStore.size(),
31
35
  });
@@ -50,49 +54,58 @@ function dispatchTransportRequest(transport, req, res, body) {
50
54
  : transport.handleRequest(req, res);
51
55
  }
52
56
  function resolveSessionTransport(sessionId, options, res) {
57
+ const { sessionStore } = options;
53
58
  if (!sessionId) {
54
59
  respondMissingSession(res);
55
60
  return null;
56
61
  }
57
- const session = options.sessionStore.get(sessionId);
62
+ const session = sessionStore.get(sessionId);
58
63
  if (!session) {
59
64
  respondSessionNotFound(res);
60
65
  return null;
61
66
  }
62
- options.sessionStore.touch(sessionId);
67
+ sessionStore.touch(sessionId);
63
68
  return session.transport;
64
69
  }
65
70
  async function handlePost(req, res, options) {
71
+ ensurePostAcceptHeader(req);
72
+ if (!ensureMcpProtocolVersionHeader(req, res))
73
+ return;
66
74
  const sessionId = getSessionId(req);
67
- const { body } = req;
68
- if (!isMcpRequestBody(body)) {
69
- respondInvalidRequestBody(res);
75
+ const payload = validatePostPayload(req.body, res);
76
+ if (!payload)
70
77
  return;
71
- }
72
- logPostRequest(body, sessionId, options);
73
- const transport = await resolveTransportForPost(req, res, body, sessionId, options);
78
+ logPostRequest(payload, sessionId, options);
79
+ const transport = await resolveTransportForPost(req, res, payload, sessionId, options);
74
80
  if (!transport)
75
81
  return;
76
- await handleTransportRequest(transport, req, res, body);
82
+ await handleTransportRequest(transport, req, res, payload);
77
83
  }
78
84
  async function handleGet(req, res, options) {
85
+ if (!ensureMcpProtocolVersionHeader(req, res))
86
+ return;
87
+ if (!acceptsEventStream(req)) {
88
+ res.status(406).json({
89
+ error: 'Not Acceptable',
90
+ code: 'ACCEPT_NOT_SUPPORTED',
91
+ });
92
+ return;
93
+ }
79
94
  const transport = resolveSessionTransport(getSessionId(req), options, res);
80
95
  if (!transport)
81
96
  return;
82
97
  await handleTransportRequest(transport, req, res);
83
98
  }
84
99
  async function handleDelete(req, res, options) {
100
+ if (!ensureMcpProtocolVersionHeader(req, res))
101
+ return;
85
102
  const transport = resolveSessionTransport(getSessionId(req), options, res);
86
103
  if (!transport)
87
104
  return;
88
105
  await handleTransportRequest(transport, req, res);
89
106
  }
90
107
  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)));
108
+ app.post('/mcp', wrapAsync((req, res) => handlePost(req, res, options)));
109
+ app.get('/mcp', wrapAsync((req, res) => handleGet(req, res, options)));
110
+ app.delete('/mcp', wrapAsync((req, res) => handleDelete(req, res, options)));
97
111
  }
98
- export { evictExpiredSessions } from './mcp-session.js';
@@ -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 { 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
+ }