@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/auth.js
CHANGED
|
@@ -1,38 +1,269 @@
|
|
|
1
|
+
import { InvalidTokenError, ServerError, } from '@modelcontextprotocol/sdk/server/auth/errors.js';
|
|
2
|
+
import { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js';
|
|
3
|
+
import { getOAuthProtectedResourceMetadataUrl, mcpAuthMetadataRouter, } from '@modelcontextprotocol/sdk/server/auth/router.js';
|
|
4
|
+
import { config } from '../config/index.js';
|
|
1
5
|
import { timingSafeEqualUtf8 } from '../utils/crypto.js';
|
|
2
|
-
|
|
3
|
-
|
|
6
|
+
import { isRecord } from '../utils/guards.js';
|
|
7
|
+
const STATIC_TOKEN_TTL_SECONDS = 60 * 60 * 24;
|
|
8
|
+
function stripHash(url) {
|
|
9
|
+
const copy = new URL(url.href);
|
|
10
|
+
copy.hash = '';
|
|
11
|
+
return copy.href;
|
|
12
|
+
}
|
|
13
|
+
function parseScopes(value) {
|
|
14
|
+
if (typeof value === 'string') {
|
|
15
|
+
return value
|
|
16
|
+
.split(' ')
|
|
17
|
+
.map((scope) => scope.trim())
|
|
18
|
+
.filter((scope) => scope.length > 0);
|
|
19
|
+
}
|
|
20
|
+
if (Array.isArray(value)) {
|
|
21
|
+
return value.filter((scope) => typeof scope === 'string');
|
|
22
|
+
}
|
|
23
|
+
return [];
|
|
4
24
|
}
|
|
5
|
-
function
|
|
6
|
-
|
|
25
|
+
function parseResourceUrl(value) {
|
|
26
|
+
if (typeof value !== 'string')
|
|
27
|
+
return undefined;
|
|
28
|
+
if (!URL.canParse(value))
|
|
29
|
+
return undefined;
|
|
30
|
+
return new URL(value);
|
|
7
31
|
}
|
|
8
|
-
function
|
|
9
|
-
if (
|
|
10
|
-
return
|
|
11
|
-
|
|
12
|
-
if (
|
|
13
|
-
|
|
32
|
+
function parseAudResource(aud) {
|
|
33
|
+
if (typeof aud === 'string') {
|
|
34
|
+
return parseResourceUrl(aud);
|
|
35
|
+
}
|
|
36
|
+
if (Array.isArray(aud)) {
|
|
37
|
+
for (const entry of aud) {
|
|
38
|
+
const parsed = parseResourceUrl(entry);
|
|
39
|
+
if (parsed)
|
|
40
|
+
return parsed;
|
|
41
|
+
}
|
|
14
42
|
}
|
|
15
|
-
|
|
16
|
-
return apiKeyHeader ? timingSafeEquals(apiKeyHeader, authToken) : false;
|
|
43
|
+
return undefined;
|
|
17
44
|
}
|
|
18
|
-
function
|
|
19
|
-
const
|
|
20
|
-
if (
|
|
21
|
-
return
|
|
22
|
-
|
|
23
|
-
|
|
45
|
+
function extractResource(data) {
|
|
46
|
+
const resource = parseResourceUrl(data.resource);
|
|
47
|
+
if (resource)
|
|
48
|
+
return resource;
|
|
49
|
+
return parseAudResource(data.aud);
|
|
50
|
+
}
|
|
51
|
+
function extractScopes(data) {
|
|
52
|
+
if (data.scope !== undefined) {
|
|
53
|
+
return parseScopes(data.scope);
|
|
54
|
+
}
|
|
55
|
+
if (data.scopes !== undefined) {
|
|
56
|
+
return parseScopes(data.scopes);
|
|
57
|
+
}
|
|
58
|
+
if (data.scp !== undefined) {
|
|
59
|
+
return parseScopes(data.scp);
|
|
60
|
+
}
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
function readExpiresAt(data) {
|
|
64
|
+
const expiresAt = typeof data.exp === 'number' ? data.exp : Number.NaN;
|
|
65
|
+
if (!Number.isFinite(expiresAt)) {
|
|
66
|
+
throw new InvalidTokenError('Token has no expiration time');
|
|
67
|
+
}
|
|
68
|
+
return expiresAt;
|
|
69
|
+
}
|
|
70
|
+
function resolveClientId(data) {
|
|
71
|
+
if (typeof data.client_id === 'string')
|
|
72
|
+
return data.client_id;
|
|
73
|
+
if (typeof data.cid === 'string')
|
|
74
|
+
return data.cid;
|
|
75
|
+
if (typeof data.sub === 'string')
|
|
76
|
+
return data.sub;
|
|
77
|
+
return 'unknown';
|
|
78
|
+
}
|
|
79
|
+
function ensureResourceMatch(resource) {
|
|
80
|
+
if (resource && stripHash(resource) !== stripHash(config.auth.resourceUrl)) {
|
|
81
|
+
throw new InvalidTokenError('Token resource mismatch');
|
|
82
|
+
}
|
|
83
|
+
return resource;
|
|
84
|
+
}
|
|
85
|
+
function buildIntrospectionAuthInfo(token, data) {
|
|
86
|
+
const resource = ensureResourceMatch(extractResource(data));
|
|
87
|
+
return {
|
|
88
|
+
token,
|
|
89
|
+
clientId: resolveClientId(data),
|
|
90
|
+
scopes: extractScopes(data),
|
|
91
|
+
expiresAt: readExpiresAt(data),
|
|
92
|
+
resource: resource ?? config.auth.resourceUrl,
|
|
93
|
+
extra: data,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function buildBasicAuthHeader(clientId, clientSecret) {
|
|
97
|
+
const secret = clientSecret ?? '';
|
|
98
|
+
const basic = Buffer.from(`${clientId}:${secret}`, 'utf8').toString('base64');
|
|
99
|
+
return `Basic ${basic}`;
|
|
100
|
+
}
|
|
101
|
+
function buildIntrospectionRequest(token, resourceUrl, clientId, clientSecret) {
|
|
102
|
+
const body = new URLSearchParams({
|
|
103
|
+
token,
|
|
104
|
+
token_type_hint: 'access_token',
|
|
105
|
+
resource: stripHash(resourceUrl),
|
|
106
|
+
}).toString();
|
|
107
|
+
const headers = {
|
|
108
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
109
|
+
};
|
|
110
|
+
if (clientId) {
|
|
111
|
+
headers.authorization = buildBasicAuthHeader(clientId, clientSecret);
|
|
112
|
+
}
|
|
113
|
+
return { body, headers };
|
|
114
|
+
}
|
|
115
|
+
async function requestIntrospection(introspectionUrl, request, timeoutMs) {
|
|
116
|
+
const response = await fetch(introspectionUrl, {
|
|
117
|
+
method: 'POST',
|
|
118
|
+
headers: request.headers,
|
|
119
|
+
body: request.body,
|
|
120
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
121
|
+
});
|
|
122
|
+
if (!response.ok) {
|
|
123
|
+
await response.body?.cancel();
|
|
124
|
+
throw new ServerError(`Token introspection failed: ${response.status}`);
|
|
125
|
+
}
|
|
126
|
+
return response.json();
|
|
127
|
+
}
|
|
128
|
+
function parseIntrospectionPayload(payload) {
|
|
129
|
+
if (!isRecord(payload) || Array.isArray(payload)) {
|
|
130
|
+
throw new ServerError('Invalid introspection response');
|
|
131
|
+
}
|
|
132
|
+
if (payload.active !== true) {
|
|
133
|
+
throw new InvalidTokenError('Token is inactive');
|
|
134
|
+
}
|
|
135
|
+
return payload;
|
|
136
|
+
}
|
|
137
|
+
async function verifyWithIntrospection(token) {
|
|
138
|
+
const { auth } = config;
|
|
139
|
+
if (!auth.introspectionUrl) {
|
|
140
|
+
throw new ServerError('Token introspection is not configured');
|
|
141
|
+
}
|
|
142
|
+
const request = buildIntrospectionRequest(token, auth.resourceUrl, auth.clientId, auth.clientSecret);
|
|
143
|
+
const payload = await requestIntrospection(auth.introspectionUrl, request, auth.introspectionTimeoutMs);
|
|
144
|
+
return buildIntrospectionAuthInfo(token, parseIntrospectionPayload(payload));
|
|
145
|
+
}
|
|
146
|
+
function buildStaticAuthInfo(token) {
|
|
147
|
+
return {
|
|
148
|
+
token,
|
|
149
|
+
clientId: 'static-token',
|
|
150
|
+
scopes: config.auth.requiredScopes,
|
|
151
|
+
expiresAt: Math.floor(Date.now() / 1000) + STATIC_TOKEN_TTL_SECONDS,
|
|
152
|
+
resource: config.auth.resourceUrl,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function verifyStaticToken(token) {
|
|
156
|
+
if (config.auth.staticTokens.length === 0) {
|
|
157
|
+
throw new InvalidTokenError('No static tokens configured');
|
|
158
|
+
}
|
|
159
|
+
const matched = config.auth.staticTokens.some((candidate) => timingSafeEqualUtf8(candidate, token));
|
|
160
|
+
if (!matched) {
|
|
161
|
+
throw new InvalidTokenError('Invalid token');
|
|
162
|
+
}
|
|
163
|
+
return buildStaticAuthInfo(token);
|
|
164
|
+
}
|
|
165
|
+
function normalizeHeaderValue(header) {
|
|
166
|
+
return Array.isArray(header) ? header[0] : header;
|
|
24
167
|
}
|
|
25
168
|
function getApiKeyHeader(req) {
|
|
26
169
|
const apiKeyHeader = normalizeHeaderValue(req.headers['x-api-key']);
|
|
27
170
|
return apiKeyHeader ? apiKeyHeader.trim() : null;
|
|
28
171
|
}
|
|
29
|
-
|
|
30
|
-
return (req,
|
|
31
|
-
if (
|
|
172
|
+
function createLegacyApiKeyMiddleware() {
|
|
173
|
+
return (req, _res, next) => {
|
|
174
|
+
if (config.auth.mode !== 'static') {
|
|
32
175
|
next();
|
|
33
176
|
return;
|
|
34
177
|
}
|
|
35
|
-
|
|
36
|
-
|
|
178
|
+
if (!req.headers.authorization) {
|
|
179
|
+
const apiKey = getApiKeyHeader(req);
|
|
180
|
+
if (apiKey) {
|
|
181
|
+
req.headers.authorization = `Bearer ${apiKey}`;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
next();
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
async function verifyAccessToken(token) {
|
|
188
|
+
if (config.auth.mode === 'oauth') {
|
|
189
|
+
return verifyWithIntrospection(token);
|
|
190
|
+
}
|
|
191
|
+
return verifyStaticToken(token);
|
|
192
|
+
}
|
|
193
|
+
function resolveMetadataUrl() {
|
|
194
|
+
if (config.auth.mode !== 'oauth')
|
|
195
|
+
return null;
|
|
196
|
+
return getOAuthProtectedResourceMetadataUrl(new URL(config.auth.resourceUrl));
|
|
197
|
+
}
|
|
198
|
+
function resolveOptionalScopes(requiredScopes) {
|
|
199
|
+
return requiredScopes.length > 0 ? [...requiredScopes] : undefined;
|
|
200
|
+
}
|
|
201
|
+
function resolveOAuthMetadataParams(authConfig) {
|
|
202
|
+
const { issuerUrl, authorizationUrl, tokenUrl, revocationUrl, registrationUrl, requiredScopes, } = authConfig;
|
|
203
|
+
if (!issuerUrl || !authorizationUrl || !tokenUrl)
|
|
204
|
+
return null;
|
|
205
|
+
return {
|
|
206
|
+
issuerUrl,
|
|
207
|
+
authorizationUrl,
|
|
208
|
+
tokenUrl,
|
|
209
|
+
revocationUrl,
|
|
210
|
+
registrationUrl,
|
|
211
|
+
requiredScopes,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function buildBaseOAuthMetadata(params) {
|
|
215
|
+
return {
|
|
216
|
+
issuer: params.issuerUrl.href,
|
|
217
|
+
authorization_endpoint: params.authorizationUrl.href,
|
|
218
|
+
response_types_supported: ['code'],
|
|
219
|
+
code_challenge_methods_supported: ['S256'],
|
|
220
|
+
token_endpoint: params.tokenUrl.href,
|
|
221
|
+
token_endpoint_auth_methods_supported: ['client_secret_post', 'none'],
|
|
222
|
+
grant_types_supported: ['authorization_code', 'refresh_token'],
|
|
37
223
|
};
|
|
38
224
|
}
|
|
225
|
+
function applyOptionalScopes(metadata, requiredScopes) {
|
|
226
|
+
const scopesSupported = resolveOptionalScopes(requiredScopes);
|
|
227
|
+
if (scopesSupported !== undefined) {
|
|
228
|
+
metadata.scopes_supported = scopesSupported;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function applyOptionalEndpoint(metadata, key, url) {
|
|
232
|
+
if (!url)
|
|
233
|
+
return;
|
|
234
|
+
metadata[key] = url.href;
|
|
235
|
+
}
|
|
236
|
+
function buildOAuthMetadata(params) {
|
|
237
|
+
const oauthMetadata = buildBaseOAuthMetadata(params);
|
|
238
|
+
applyOptionalScopes(oauthMetadata, params.requiredScopes);
|
|
239
|
+
applyOptionalEndpoint(oauthMetadata, 'revocation_endpoint', params.revocationUrl);
|
|
240
|
+
applyOptionalEndpoint(oauthMetadata, 'registration_endpoint', params.registrationUrl);
|
|
241
|
+
return oauthMetadata;
|
|
242
|
+
}
|
|
243
|
+
export function createAuthMiddleware() {
|
|
244
|
+
const metadataUrl = resolveMetadataUrl();
|
|
245
|
+
const authHandler = requireBearerAuth({
|
|
246
|
+
verifier: { verifyAccessToken },
|
|
247
|
+
requiredScopes: config.auth.requiredScopes,
|
|
248
|
+
...(metadataUrl ? { resourceMetadataUrl: metadataUrl } : {}),
|
|
249
|
+
});
|
|
250
|
+
const legacyHandler = createLegacyApiKeyMiddleware();
|
|
251
|
+
return (req, res, next) => {
|
|
252
|
+
legacyHandler(req, res, () => {
|
|
253
|
+
authHandler(req, res, next);
|
|
254
|
+
});
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
export function createAuthMetadataRouter() {
|
|
258
|
+
if (config.auth.mode !== 'oauth')
|
|
259
|
+
return null;
|
|
260
|
+
const oauthMetadataParams = resolveOAuthMetadataParams(config.auth);
|
|
261
|
+
if (!oauthMetadataParams)
|
|
262
|
+
return null;
|
|
263
|
+
return mcpAuthMetadataRouter({
|
|
264
|
+
oauthMetadata: buildOAuthMetadata(oauthMetadataParams),
|
|
265
|
+
resourceServerUrl: config.auth.resourceUrl,
|
|
266
|
+
scopesSupported: config.auth.requiredScopes,
|
|
267
|
+
resourceName: config.server.name,
|
|
268
|
+
});
|
|
269
|
+
}
|
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
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { config } from '../config/index.js';
|
|
2
|
+
const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1']);
|
|
3
|
+
function getNonEmptyStringHeader(value) {
|
|
4
|
+
if (typeof value !== 'string')
|
|
5
|
+
return null;
|
|
6
|
+
const trimmed = value.trim();
|
|
7
|
+
return trimmed === '' ? null : trimmed;
|
|
8
|
+
}
|
|
9
|
+
function respondHostNotAllowed(res) {
|
|
10
|
+
res.status(403).json({
|
|
11
|
+
error: 'Host not allowed',
|
|
12
|
+
code: 'HOST_NOT_ALLOWED',
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
function respondOriginNotAllowed(res) {
|
|
16
|
+
res.status(403).json({
|
|
17
|
+
error: 'Origin not allowed',
|
|
18
|
+
code: 'ORIGIN_NOT_ALLOWED',
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function tryParseOriginHostname(originHeader) {
|
|
22
|
+
try {
|
|
23
|
+
return new URL(originHeader).hostname.toLowerCase();
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function takeFirstHostValue(value) {
|
|
30
|
+
const first = value.split(',')[0];
|
|
31
|
+
if (!first)
|
|
32
|
+
return null;
|
|
33
|
+
const trimmed = first.trim();
|
|
34
|
+
return trimmed ? trimmed : null;
|
|
35
|
+
}
|
|
36
|
+
function stripIpv6Brackets(value) {
|
|
37
|
+
if (!value.startsWith('['))
|
|
38
|
+
return null;
|
|
39
|
+
const end = value.indexOf(']');
|
|
40
|
+
if (end === -1)
|
|
41
|
+
return null;
|
|
42
|
+
return value.slice(1, end);
|
|
43
|
+
}
|
|
44
|
+
function stripPortIfPresent(value) {
|
|
45
|
+
const colonIndex = value.indexOf(':');
|
|
46
|
+
if (colonIndex === -1)
|
|
47
|
+
return value;
|
|
48
|
+
return value.slice(0, colonIndex);
|
|
49
|
+
}
|
|
50
|
+
function normalizeHost(value) {
|
|
51
|
+
const trimmed = value.trim().toLowerCase();
|
|
52
|
+
if (!trimmed)
|
|
53
|
+
return null;
|
|
54
|
+
const first = takeFirstHostValue(trimmed);
|
|
55
|
+
if (!first)
|
|
56
|
+
return null;
|
|
57
|
+
const ipv6 = stripIpv6Brackets(first);
|
|
58
|
+
if (ipv6)
|
|
59
|
+
return ipv6;
|
|
60
|
+
return stripPortIfPresent(first);
|
|
61
|
+
}
|
|
62
|
+
function isWildcardHost(host) {
|
|
63
|
+
return host === '0.0.0.0' || host === '::';
|
|
64
|
+
}
|
|
65
|
+
function addLoopbackHosts(allowedHosts) {
|
|
66
|
+
for (const host of LOOPBACK_HOSTS) {
|
|
67
|
+
allowedHosts.add(host);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function addConfiguredHost(allowedHosts) {
|
|
71
|
+
const configuredHost = normalizeHost(config.server.host);
|
|
72
|
+
if (!configuredHost)
|
|
73
|
+
return;
|
|
74
|
+
if (isWildcardHost(configuredHost))
|
|
75
|
+
return;
|
|
76
|
+
allowedHosts.add(configuredHost);
|
|
77
|
+
}
|
|
78
|
+
function addExplicitAllowedHosts(allowedHosts) {
|
|
79
|
+
for (const host of config.security.allowedHosts) {
|
|
80
|
+
allowedHosts.add(host);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function buildAllowedHosts() {
|
|
84
|
+
const allowedHosts = new Set();
|
|
85
|
+
addLoopbackHosts(allowedHosts);
|
|
86
|
+
addConfiguredHost(allowedHosts);
|
|
87
|
+
addExplicitAllowedHosts(allowedHosts);
|
|
88
|
+
return allowedHosts;
|
|
89
|
+
}
|
|
90
|
+
export function createHostValidationMiddleware() {
|
|
91
|
+
const allowedHosts = buildAllowedHosts();
|
|
92
|
+
return (req, res, next) => {
|
|
93
|
+
const hostHeader = typeof req.headers.host === 'string' ? req.headers.host : '';
|
|
94
|
+
const normalized = normalizeHost(hostHeader);
|
|
95
|
+
if (!normalized || !allowedHosts.has(normalized)) {
|
|
96
|
+
respondHostNotAllowed(res);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
next();
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export function createOriginValidationMiddleware() {
|
|
103
|
+
const allowedHosts = buildAllowedHosts();
|
|
104
|
+
return (req, res, next) => {
|
|
105
|
+
const originHeader = getNonEmptyStringHeader(req.headers.origin);
|
|
106
|
+
if (!originHeader) {
|
|
107
|
+
next();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const originHostname = tryParseOriginHostname(originHeader);
|
|
111
|
+
if (!originHostname || !allowedHosts.has(originHostname)) {
|
|
112
|
+
respondOriginNotAllowed(res);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
next();
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type { Express } from 'express';
|
|
2
|
-
import {
|
|
1
|
+
import type { Express, Request, Response } from 'express';
|
|
2
|
+
import type { McpRequestBody } from '../config/types/runtime.js';
|
|
3
|
+
import { type McpSessionOptions } from './mcp-sessions.js';
|
|
4
|
+
export declare function isJsonRpcBatchRequest(body: unknown): boolean;
|
|
5
|
+
export declare function isMcpRequestBody(body: unknown): body is McpRequestBody;
|
|
6
|
+
export declare function ensureMcpProtocolVersionHeader(req: Request, res: Response): boolean;
|
|
7
|
+
export declare function ensurePostAcceptHeader(req: Request): void;
|
|
8
|
+
export declare function acceptsEventStream(req: Request): boolean;
|
|
3
9
|
export declare function registerMcpRoutes(app: Express, options: McpSessionOptions): void;
|
|
4
|
-
export { evictExpiredSessions } from './mcp-session.js';
|