@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.
- package/README.md +116 -152
- 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 +25 -59
- 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 +98 -26
- 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/jsonrpc-http.d.ts +2 -0
- package/dist/http/jsonrpc-http.js +10 -0
- package/dist/http/mcp-routes.d.ts +0 -1
- package/dist/http/mcp-routes.js +43 -30
- package/dist/http/mcp-session-helpers.d.ts +0 -1
- package/dist/http/mcp-session-helpers.js +1 -1
- package/dist/http/mcp-session-transport.d.ts +7 -0
- package/dist/http/mcp-session-transport.js +57 -0
- package/dist/http/mcp-session.js +60 -73
- 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 +5 -2
- package/dist/http/server-config.d.ts +1 -0
- package/dist/http/server-config.js +40 -0
- package/dist/http/server-middleware.d.ts +2 -9
- package/dist/http/server-middleware.js +96 -43
- package/dist/http/server-shutdown.d.ts +4 -0
- package/dist/http/server-shutdown.js +43 -0
- package/dist/http/server.js +52 -64
- package/dist/http/session-cleanup.js +1 -1
- package/dist/middleware/error-handler.js +1 -3
- package/dist/resources/cached-content.js +50 -108
- package/dist/resources/index.js +0 -82
- package/dist/server.js +51 -30
- package/dist/services/cache-keys.d.ts +7 -0
- package/dist/services/cache-keys.js +57 -0
- package/dist/services/cache.d.ts +1 -7
- package/dist/services/cache.js +53 -119
- package/dist/services/context.d.ts +0 -1
- package/dist/services/context.js +0 -7
- package/dist/services/extractor.js +10 -82
- package/dist/services/fetcher/agents.d.ts +2 -2
- package/dist/services/fetcher/agents.js +34 -95
- 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 +30 -13
- package/dist/services/fetcher/redirects.js +4 -3
- package/dist/services/fetcher/response.js +66 -31
- package/dist/services/fetcher.d.ts +1 -3
- package/dist/services/fetcher.js +14 -33
- 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 +1 -20
- package/dist/tools/handlers/fetch-single.shared.js +44 -87
- package/dist/tools/handlers/fetch-url.tool.d.ts +1 -1
- package/dist/tools/handlers/fetch-url.tool.js +46 -123
- 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 +52 -0
- package/dist/tools/utils/content-transform.d.ts +2 -17
- package/dist/tools/utils/content-transform.js +120 -114
- package/dist/tools/utils/fetch-pipeline.d.ts +0 -8
- package/dist/tools/utils/fetch-pipeline.js +65 -62
- package/dist/tools/utils/inline-content.d.ts +1 -2
- package/dist/tools/utils/inline-content.js +4 -7
- package/dist/transformers/markdown.transformer.js +109 -34
- package/dist/utils/cached-payload.d.ts +7 -0
- package/dist/utils/cached-payload.js +36 -0
- package/dist/utils/error-utils.js +1 -1
- package/dist/utils/filename-generator.js +21 -10
- 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/tool-error-handler.d.ts +2 -2
- package/dist/utils/tool-error-handler.js +11 -38
- 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 +20 -93
- package/dist/workers/content-transform.worker.d.ts +1 -0
- package/dist/workers/content-transform.worker.js +40 -0
- package/package.json +13 -16
package/dist/http/auth.js
CHANGED
|
@@ -1,38 +1,110 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
30
|
-
return (req,
|
|
31
|
-
if (
|
|
13
|
+
function createLegacyApiKeyMiddleware() {
|
|
14
|
+
return (req, _res, next) => {
|
|
15
|
+
if (config.auth.mode !== 'static') {
|
|
32
16
|
next();
|
|
33
17
|
return;
|
|
34
18
|
}
|
|
35
|
-
|
|
36
|
-
|
|
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
|
+
}
|
package/dist/http/cors.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { NextFunction, Request, Response } from 'express';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6
|
+
import { wrapAsync } from './async-handler.js';
|
|
6
7
|
const HASH_PATTERN = /^[a-f0-9.]+$/i;
|
|
7
8
|
function validateNamespace(namespace) {
|
|
8
|
-
return
|
|
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
|
|
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 =
|
|
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,
|
|
53
|
+
const fileName = generateSafeFilename(cacheEntry.url, cacheEntry.title ?? safeTitle, params.hash, '.md');
|
|
90
54
|
return {
|
|
91
55
|
content,
|
|
92
|
-
contentType:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
130
|
-
return Promise.resolve();
|
|
96
|
+
sendDownloadPayload(res, payload);
|
|
131
97
|
}
|
|
132
98
|
export function registerDownloadRoutes(app) {
|
|
133
|
-
|
|
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
|
}
|
package/dist/http/mcp-routes.js
CHANGED
|
@@ -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
|
|
13
|
+
sendJsonRpcError(res, -32600, 'Missing mcp-session-id header', 400);
|
|
20
14
|
}
|
|
21
15
|
function respondSessionNotFound(res) {
|
|
22
|
-
res
|
|
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 =
|
|
62
|
+
const session = sessionStore.get(sessionId);
|
|
58
63
|
if (!session) {
|
|
59
64
|
respondSessionNotFound(res);
|
|
60
65
|
return null;
|
|
61
66
|
}
|
|
62
|
-
|
|
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
|
|
68
|
-
if (!
|
|
69
|
-
respondInvalidRequestBody(res);
|
|
75
|
+
const payload = validatePostPayload(req.body, res);
|
|
76
|
+
if (!payload)
|
|
70
77
|
return;
|
|
71
|
-
|
|
72
|
-
|
|
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,
|
|
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
|
-
|
|
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)));
|
|
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;
|
|
@@ -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
|
+
}
|