@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
|
@@ -1,57 +1,56 @@
|
|
|
1
1
|
import { FetchError } from '../errors/app-error.js';
|
|
2
2
|
import { logError } from '../services/logger.js';
|
|
3
|
-
function getStatusCode(
|
|
4
|
-
return
|
|
3
|
+
function getStatusCode(fetchError) {
|
|
4
|
+
return fetchError ? fetchError.statusCode : 500;
|
|
5
5
|
}
|
|
6
|
-
function getErrorCode(
|
|
7
|
-
return
|
|
6
|
+
function getErrorCode(fetchError) {
|
|
7
|
+
return fetchError ? fetchError.code : 'INTERNAL_ERROR';
|
|
8
8
|
}
|
|
9
|
-
function getErrorMessage(
|
|
10
|
-
return
|
|
9
|
+
function getErrorMessage(fetchError) {
|
|
10
|
+
return fetchError ? fetchError.message : 'Internal Server Error';
|
|
11
11
|
}
|
|
12
|
-
function getErrorDetails(
|
|
13
|
-
if (
|
|
14
|
-
return
|
|
12
|
+
function getErrorDetails(fetchError) {
|
|
13
|
+
if (fetchError && Object.keys(fetchError.details).length > 0) {
|
|
14
|
+
return fetchError.details;
|
|
15
15
|
}
|
|
16
16
|
return undefined;
|
|
17
17
|
}
|
|
18
|
-
function setRetryAfterHeader(res,
|
|
19
|
-
const retryAfter = resolveRetryAfter(
|
|
20
|
-
if (
|
|
18
|
+
function setRetryAfterHeader(res, fetchError) {
|
|
19
|
+
const retryAfter = resolveRetryAfter(fetchError);
|
|
20
|
+
if (retryAfter === undefined)
|
|
21
21
|
return;
|
|
22
22
|
res.set('Retry-After', retryAfter);
|
|
23
23
|
}
|
|
24
|
-
function buildErrorResponse(
|
|
25
|
-
const details = getErrorDetails(
|
|
24
|
+
function buildErrorResponse(fetchError) {
|
|
25
|
+
const details = getErrorDetails(fetchError);
|
|
26
26
|
const response = {
|
|
27
27
|
error: {
|
|
28
|
-
message: getErrorMessage(
|
|
29
|
-
code: getErrorCode(
|
|
30
|
-
statusCode: getStatusCode(
|
|
28
|
+
message: getErrorMessage(fetchError),
|
|
29
|
+
code: getErrorCode(fetchError),
|
|
30
|
+
statusCode: getStatusCode(fetchError),
|
|
31
31
|
...(details && { details }),
|
|
32
32
|
},
|
|
33
33
|
};
|
|
34
|
-
|
|
35
|
-
response.error.stack = err.stack;
|
|
36
|
-
}
|
|
34
|
+
// Never expose stack traces in production
|
|
37
35
|
return response;
|
|
38
36
|
}
|
|
39
|
-
function resolveRetryAfter(
|
|
40
|
-
if (
|
|
41
|
-
return
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const { retryAfter } = err.details;
|
|
45
|
-
if (!isRetryAfterValue(retryAfter))
|
|
46
|
-
return null;
|
|
47
|
-
return String(retryAfter);
|
|
37
|
+
function resolveRetryAfter(fetchError) {
|
|
38
|
+
if (fetchError?.statusCode !== 429)
|
|
39
|
+
return undefined;
|
|
40
|
+
const { retryAfter } = fetchError.details;
|
|
41
|
+
return isRetryAfterValue(retryAfter) ? String(retryAfter) : undefined;
|
|
48
42
|
}
|
|
49
43
|
function isRetryAfterValue(value) {
|
|
50
44
|
return typeof value === 'number' || typeof value === 'string';
|
|
51
45
|
}
|
|
52
|
-
export function errorHandler(err, req, res,
|
|
53
|
-
|
|
46
|
+
export function errorHandler(err, req, res, next) {
|
|
47
|
+
if (res.headersSent) {
|
|
48
|
+
next(err);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const fetchError = err instanceof FetchError ? err : null;
|
|
52
|
+
const statusCode = getStatusCode(fetchError);
|
|
54
53
|
logError(`HTTP ${statusCode}: ${err.message} - ${req.method} ${req.path}`, err);
|
|
55
|
-
setRetryAfterHeader(res,
|
|
56
|
-
res.status(statusCode).json(buildErrorResponse(
|
|
54
|
+
setRetryAfterHeader(res, fetchError);
|
|
55
|
+
res.status(statusCode).json(buildErrorResponse(fetchError));
|
|
57
56
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { isRecord } from '../utils/guards.js';
|
|
3
|
+
export const CACHE_NAMESPACE = 'markdown';
|
|
4
|
+
const HASH_PATTERN = /^[a-f0-9.]+$/i;
|
|
5
|
+
export function resolveCacheParams(params) {
|
|
6
|
+
const parsed = requireRecordParams(params);
|
|
7
|
+
const namespace = requireParamString(parsed, 'namespace');
|
|
8
|
+
const urlHash = requireParamString(parsed, 'urlHash');
|
|
9
|
+
if (!isValidNamespace(namespace) || !isValidHash(urlHash)) {
|
|
10
|
+
throw new McpError(ErrorCode.InvalidParams, 'Invalid cache resource parameters');
|
|
11
|
+
}
|
|
12
|
+
return { namespace, urlHash };
|
|
13
|
+
}
|
|
14
|
+
function requireRecordParams(value) {
|
|
15
|
+
if (!isRecord(value)) {
|
|
16
|
+
throw new McpError(ErrorCode.InvalidParams, 'Invalid cache resource parameters');
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function requireParamString(params, key) {
|
|
21
|
+
const raw = params[key];
|
|
22
|
+
const resolved = resolveStringParam(raw);
|
|
23
|
+
if (!resolved) {
|
|
24
|
+
throw new McpError(ErrorCode.InvalidParams, 'Both namespace and urlHash parameters are required');
|
|
25
|
+
}
|
|
26
|
+
return resolved;
|
|
27
|
+
}
|
|
28
|
+
function isValidNamespace(namespace) {
|
|
29
|
+
return namespace === CACHE_NAMESPACE;
|
|
30
|
+
}
|
|
31
|
+
function isValidHash(hash) {
|
|
32
|
+
return HASH_PATTERN.test(hash) && hash.length >= 8 && hash.length <= 64;
|
|
33
|
+
}
|
|
34
|
+
function resolveStringParam(value) {
|
|
35
|
+
return typeof value === 'string' ? value : null;
|
|
36
|
+
}
|
|
@@ -1,45 +1,65 @@
|
|
|
1
1
|
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
3
3
|
import * as cache from '../services/cache.js';
|
|
4
|
+
import { parseCacheKey, toResourceUri } from '../services/cache-keys.js';
|
|
4
5
|
import { logWarn } from '../services/logger.js';
|
|
5
|
-
import {
|
|
6
|
-
|
|
6
|
+
import { parseCachedPayload, resolveCachedPayloadContent, } from '../utils/cached-payload.js';
|
|
7
|
+
import { getErrorMessage } from '../utils/error-details.js';
|
|
8
|
+
import { isRecord } from '../utils/guards.js';
|
|
9
|
+
const CACHE_NAMESPACE = 'markdown';
|
|
7
10
|
const HASH_PATTERN = /^[a-f0-9.]+$/i;
|
|
11
|
+
function resolveCacheParams(params) {
|
|
12
|
+
const parsed = requireRecordParams(params);
|
|
13
|
+
const namespace = requireParamString(parsed, 'namespace');
|
|
14
|
+
const urlHash = requireParamString(parsed, 'urlHash');
|
|
15
|
+
if (!isValidNamespace(namespace) || !isValidHash(urlHash)) {
|
|
16
|
+
throw new McpError(ErrorCode.InvalidParams, 'Invalid cache resource parameters');
|
|
17
|
+
}
|
|
18
|
+
return { namespace, urlHash };
|
|
19
|
+
}
|
|
20
|
+
function requireRecordParams(value) {
|
|
21
|
+
if (!isRecord(value)) {
|
|
22
|
+
throw new McpError(ErrorCode.InvalidParams, 'Invalid cache resource parameters');
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
function requireParamString(params, key) {
|
|
27
|
+
const raw = params[key];
|
|
28
|
+
const resolved = resolveStringParam(raw);
|
|
29
|
+
if (!resolved) {
|
|
30
|
+
throw new McpError(ErrorCode.InvalidParams, 'Both namespace and urlHash parameters are required');
|
|
31
|
+
}
|
|
32
|
+
return resolved;
|
|
33
|
+
}
|
|
34
|
+
function isValidNamespace(namespace) {
|
|
35
|
+
return namespace === CACHE_NAMESPACE;
|
|
36
|
+
}
|
|
37
|
+
function isValidHash(hash) {
|
|
38
|
+
return HASH_PATTERN.test(hash) && hash.length >= 8 && hash.length <= 64;
|
|
39
|
+
}
|
|
40
|
+
function resolveStringParam(value) {
|
|
41
|
+
return typeof value === 'string' ? value : null;
|
|
42
|
+
}
|
|
8
43
|
function buildResourceEntry(namespace, urlHash) {
|
|
9
44
|
return {
|
|
10
45
|
name: `${namespace}:${urlHash}`,
|
|
11
46
|
uri: `superfetch://cache/${namespace}/${urlHash}`,
|
|
12
47
|
description: `Cached content entry for ${namespace}`,
|
|
13
|
-
mimeType:
|
|
48
|
+
mimeType: 'text/markdown',
|
|
14
49
|
};
|
|
15
50
|
}
|
|
16
51
|
function listCachedResources() {
|
|
17
52
|
const resources = cache
|
|
18
53
|
.keys()
|
|
19
54
|
.map((key) => {
|
|
20
|
-
const parts =
|
|
21
|
-
|
|
55
|
+
const parts = parseCacheKey(key);
|
|
56
|
+
if (parts?.namespace !== CACHE_NAMESPACE)
|
|
57
|
+
return null;
|
|
58
|
+
return buildResourceEntry(parts.namespace, parts.urlHash);
|
|
22
59
|
})
|
|
23
60
|
.filter((entry) => entry !== null);
|
|
24
61
|
return { resources };
|
|
25
62
|
}
|
|
26
|
-
function buildCacheListPayload() {
|
|
27
|
-
const cacheKeys = cache.keys();
|
|
28
|
-
return {
|
|
29
|
-
totalEntries: cacheKeys.length,
|
|
30
|
-
entries: cacheKeys.map((key) => {
|
|
31
|
-
const parts = cache.parseCacheKey(key);
|
|
32
|
-
const namespace = parts?.namespace ?? 'unknown';
|
|
33
|
-
const urlHash = parts?.urlHash ?? 'unknown';
|
|
34
|
-
return {
|
|
35
|
-
namespace,
|
|
36
|
-
urlHash,
|
|
37
|
-
resourceUri: `superfetch://cache/${namespace}/${urlHash}`,
|
|
38
|
-
};
|
|
39
|
-
}),
|
|
40
|
-
timestamp: new Date().toISOString(),
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
63
|
function notifyResourceUpdate(server, uri) {
|
|
44
64
|
if (!server.isConnected())
|
|
45
65
|
return;
|
|
@@ -52,88 +72,31 @@ function notifyResourceUpdate(server, uri) {
|
|
|
52
72
|
}
|
|
53
73
|
export function registerCachedContentResource(server) {
|
|
54
74
|
registerCacheContentResource(server);
|
|
55
|
-
registerCacheListResource(server);
|
|
56
75
|
registerCacheUpdateSubscription(server);
|
|
57
76
|
}
|
|
58
|
-
function
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
if (!namespace || !urlHash) {
|
|
62
|
-
throw new McpError(ErrorCode.InvalidParams, 'Both namespace and urlHash parameters are required');
|
|
63
|
-
}
|
|
64
|
-
if (!isValidNamespace(namespace) || !isValidHash(urlHash)) {
|
|
65
|
-
throw new McpError(ErrorCode.InvalidParams, 'Invalid cache resource parameters');
|
|
66
|
-
}
|
|
67
|
-
return { namespace, urlHash };
|
|
68
|
-
}
|
|
69
|
-
function buildCachedContentResponse(uri, cacheKey, namespace) {
|
|
70
|
-
const cached = cache.get(cacheKey);
|
|
71
|
-
if (!cached) {
|
|
72
|
-
throw new McpError(ErrorCode.InvalidParams, `Content not found in cache for key: ${cacheKey}. Use superfetch://stats to see available cache entries.`);
|
|
73
|
-
}
|
|
74
|
-
if (namespace !== 'url' && namespace !== 'markdown') {
|
|
75
|
-
return {
|
|
76
|
-
contents: [
|
|
77
|
-
{
|
|
78
|
-
uri: uri.href,
|
|
79
|
-
mimeType: resolveCacheMimeType(namespace),
|
|
80
|
-
text: cached.content,
|
|
81
|
-
},
|
|
82
|
-
],
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
const payload = parseCachedPayload(cached.content);
|
|
86
|
-
const resolvedContent = payload
|
|
87
|
-
? resolvePayloadContent(payload, namespace)
|
|
88
|
-
: null;
|
|
89
|
-
if (!resolvedContent) {
|
|
90
|
-
throw new McpError(ErrorCode.InternalError, `Cached content is missing for namespace ${namespace}`);
|
|
91
|
-
}
|
|
92
|
-
return {
|
|
93
|
-
contents: [
|
|
94
|
-
{
|
|
95
|
-
uri: uri.href,
|
|
96
|
-
mimeType: resolveCacheMimeType(namespace),
|
|
97
|
-
text: resolvedContent,
|
|
98
|
-
},
|
|
99
|
-
],
|
|
100
|
-
};
|
|
77
|
+
function buildCachedContentResponse(uri, cacheKey) {
|
|
78
|
+
const cached = requireCacheEntry(cacheKey);
|
|
79
|
+
return buildMarkdownContentResponse(uri, cached.content);
|
|
101
80
|
}
|
|
102
81
|
function registerCacheContentResource(server) {
|
|
103
82
|
server.registerResource('cached-content', new ResourceTemplate('superfetch://cache/{namespace}/{urlHash}', {
|
|
104
83
|
list: listCachedResources,
|
|
105
84
|
}), {
|
|
106
85
|
title: 'Cached Content',
|
|
107
|
-
description: 'Access previously fetched web content from cache. Namespace:
|
|
86
|
+
description: 'Access previously fetched web content from cache. Namespace: markdown. UrlHash: SHA-256 hash of the URL.',
|
|
108
87
|
mimeType: 'text/plain',
|
|
109
88
|
}, (uri, params) => {
|
|
110
89
|
const { namespace, urlHash } = resolveCacheParams(params);
|
|
111
90
|
const cacheKey = `${namespace}:${urlHash}`;
|
|
112
|
-
return buildCachedContentResponse(uri, cacheKey
|
|
91
|
+
return buildCachedContentResponse(uri, cacheKey);
|
|
113
92
|
});
|
|
114
93
|
}
|
|
115
|
-
function registerCacheListResource(server) {
|
|
116
|
-
server.registerResource('cached-urls', 'superfetch://cache/list', {
|
|
117
|
-
title: 'Cached URLs List',
|
|
118
|
-
description: 'List all URLs currently in cache with their namespaces',
|
|
119
|
-
mimeType: 'application/json',
|
|
120
|
-
}, (uri) => ({
|
|
121
|
-
contents: [
|
|
122
|
-
{
|
|
123
|
-
uri: uri.href,
|
|
124
|
-
mimeType: 'application/json',
|
|
125
|
-
text: JSON.stringify(buildCacheListPayload(), null, 2),
|
|
126
|
-
},
|
|
127
|
-
],
|
|
128
|
-
}));
|
|
129
|
-
}
|
|
130
94
|
function registerCacheUpdateSubscription(server) {
|
|
131
95
|
const unsubscribe = cache.onCacheUpdate(({ cacheKey }) => {
|
|
132
|
-
const resourceUri =
|
|
96
|
+
const resourceUri = toResourceUri(cacheKey);
|
|
133
97
|
if (!resourceUri)
|
|
134
98
|
return;
|
|
135
99
|
notifyResourceUpdate(server, resourceUri);
|
|
136
|
-
notifyResourceUpdate(server, 'superfetch://cache/list');
|
|
137
100
|
if (server.isConnected()) {
|
|
138
101
|
server.sendResourceListChanged();
|
|
139
102
|
}
|
|
@@ -144,47 +107,26 @@ function registerCacheUpdateSubscription(server) {
|
|
|
144
107
|
unsubscribe();
|
|
145
108
|
};
|
|
146
109
|
}
|
|
147
|
-
function
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
return 'application/jsonl';
|
|
152
|
-
return 'application/json';
|
|
153
|
-
}
|
|
154
|
-
function isValidNamespace(namespace) {
|
|
155
|
-
return VALID_NAMESPACES.has(namespace);
|
|
156
|
-
}
|
|
157
|
-
function isValidHash(hash) {
|
|
158
|
-
return HASH_PATTERN.test(hash) && hash.length >= 8 && hash.length <= 64;
|
|
159
|
-
}
|
|
160
|
-
function resolveStringParam(value) {
|
|
161
|
-
return typeof value === 'string' ? value : null;
|
|
162
|
-
}
|
|
163
|
-
function parseCachedPayload(raw) {
|
|
164
|
-
try {
|
|
165
|
-
const parsed = JSON.parse(raw);
|
|
166
|
-
return isCachedPayload(parsed) ? parsed : null;
|
|
167
|
-
}
|
|
168
|
-
catch {
|
|
169
|
-
return null;
|
|
110
|
+
function requireCacheEntry(cacheKey) {
|
|
111
|
+
const cached = cache.get(cacheKey);
|
|
112
|
+
if (!cached) {
|
|
113
|
+
throw new McpError(-32002, `Content not found in cache for key: ${cacheKey}`);
|
|
170
114
|
}
|
|
115
|
+
return cached;
|
|
171
116
|
}
|
|
172
|
-
function
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
(record.markdown === undefined || typeof record.markdown === 'string'));
|
|
178
|
-
}
|
|
179
|
-
function resolvePayloadContent(payload, namespace) {
|
|
180
|
-
if (namespace === 'markdown') {
|
|
181
|
-
if (typeof payload.markdown === 'string') {
|
|
182
|
-
return payload.markdown;
|
|
183
|
-
}
|
|
184
|
-
if (typeof payload.content === 'string') {
|
|
185
|
-
return payload.content;
|
|
186
|
-
}
|
|
187
|
-
return null;
|
|
117
|
+
function buildMarkdownContentResponse(uri, content) {
|
|
118
|
+
const payload = parseCachedPayload(content);
|
|
119
|
+
const resolvedContent = payload ? resolveCachedPayloadContent(payload) : null;
|
|
120
|
+
if (!resolvedContent) {
|
|
121
|
+
throw new McpError(ErrorCode.InternalError, 'Cached markdown content is missing');
|
|
188
122
|
}
|
|
189
|
-
return
|
|
123
|
+
return {
|
|
124
|
+
contents: [
|
|
125
|
+
{
|
|
126
|
+
uri: uri.href,
|
|
127
|
+
mimeType: 'text/markdown',
|
|
128
|
+
text: resolvedContent,
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
};
|
|
190
132
|
}
|
package/dist/resources/index.js
CHANGED
|
@@ -1,86 +1,4 @@
|
|
|
1
|
-
import { config } from '../config/index.js';
|
|
2
|
-
import * as cache from '../services/cache.js';
|
|
3
1
|
import { registerCachedContentResource } from './cached-content.js';
|
|
4
|
-
function registerJsonResource(server, definition) {
|
|
5
|
-
server.registerResource(definition.name, definition.uri, {
|
|
6
|
-
title: definition.title,
|
|
7
|
-
description: definition.description,
|
|
8
|
-
mimeType: 'application/json',
|
|
9
|
-
}, (uri) => ({
|
|
10
|
-
contents: [
|
|
11
|
-
{
|
|
12
|
-
uri: uri.href,
|
|
13
|
-
mimeType: 'application/json',
|
|
14
|
-
text: JSON.stringify(definition.buildPayload(), null, 2),
|
|
15
|
-
},
|
|
16
|
-
],
|
|
17
|
-
}));
|
|
18
|
-
}
|
|
19
|
-
function buildHealthPayload() {
|
|
20
|
-
const memUsage = process.memoryUsage();
|
|
21
|
-
const heapUsedMB = Math.round(memUsage.heapUsed / 1024 / 1024);
|
|
22
|
-
const heapTotalMB = Math.round(memUsage.heapTotal / 1024 / 1024);
|
|
23
|
-
return {
|
|
24
|
-
status: 'healthy',
|
|
25
|
-
uptime: process.uptime(),
|
|
26
|
-
checks: {
|
|
27
|
-
cache: config.cache.enabled,
|
|
28
|
-
memory: {
|
|
29
|
-
heapUsed: heapUsedMB,
|
|
30
|
-
heapTotal: heapTotalMB,
|
|
31
|
-
percentage: Math.round((heapUsedMB / heapTotalMB) * 100),
|
|
32
|
-
healthy: heapUsedMB < 400,
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
|
-
timestamp: new Date().toISOString(),
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
function buildStatsPayload() {
|
|
39
|
-
return {
|
|
40
|
-
server: {
|
|
41
|
-
name: config.server.name,
|
|
42
|
-
version: config.server.version,
|
|
43
|
-
uptime: process.uptime(),
|
|
44
|
-
nodeVersion: process.version,
|
|
45
|
-
memoryUsage: process.memoryUsage(),
|
|
46
|
-
},
|
|
47
|
-
cache: {
|
|
48
|
-
enabled: config.cache.enabled,
|
|
49
|
-
ttl: config.cache.ttl,
|
|
50
|
-
maxKeys: config.cache.maxKeys,
|
|
51
|
-
totalKeys: cache.keys().length,
|
|
52
|
-
},
|
|
53
|
-
config: {
|
|
54
|
-
fetcher: {
|
|
55
|
-
timeout: config.fetcher.timeout,
|
|
56
|
-
maxRedirects: config.fetcher.maxRedirects,
|
|
57
|
-
},
|
|
58
|
-
extraction: {
|
|
59
|
-
extractMainContent: config.extraction.extractMainContent,
|
|
60
|
-
includeMetadata: config.extraction.includeMetadata,
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
2
|
export function registerResources(server) {
|
|
66
3
|
registerCachedContentResource(server);
|
|
67
|
-
const resources = [
|
|
68
|
-
{
|
|
69
|
-
name: 'health',
|
|
70
|
-
uri: 'superfetch://health',
|
|
71
|
-
title: 'Server Health',
|
|
72
|
-
description: 'Real-time server health and dependency status',
|
|
73
|
-
buildPayload: buildHealthPayload,
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
name: 'stats',
|
|
77
|
-
uri: 'superfetch://stats',
|
|
78
|
-
title: 'Server Statistics',
|
|
79
|
-
description: 'Fetch statistics and cache performance metrics',
|
|
80
|
-
buildPayload: buildStatsPayload,
|
|
81
|
-
},
|
|
82
|
-
];
|
|
83
|
-
for (const resource of resources) {
|
|
84
|
-
registerJsonResource(server, resource);
|
|
85
|
-
}
|
|
86
4
|
}
|
package/dist/server.js
CHANGED
|
@@ -3,50 +3,64 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
3
3
|
import { config } from './config/index.js';
|
|
4
4
|
import { destroyAgents } from './services/fetcher.js';
|
|
5
5
|
import { logError, logInfo } from './services/logger.js';
|
|
6
|
-
import { destroyTransformWorkers } from './services/transform-worker-pool.js';
|
|
7
6
|
import { registerTools } from './tools/index.js';
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
import { registerCachedContentResource } from './resources/cached-content.js';
|
|
8
|
+
function createServerInfo() {
|
|
9
|
+
return {
|
|
11
10
|
name: config.server.name,
|
|
12
11
|
version: config.server.version,
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
},
|
|
19
|
-
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function createServerCapabilities() {
|
|
15
|
+
return {
|
|
16
|
+
tools: { listChanged: false },
|
|
17
|
+
resources: { listChanged: true, subscribe: true },
|
|
18
|
+
logging: {},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function createServerInstructions(serverVersion) {
|
|
22
|
+
return `superFetch MCP server |${serverVersion}| A high-performance web content fetching and processing server.`;
|
|
23
|
+
}
|
|
24
|
+
export function createMcpServer() {
|
|
25
|
+
const server = new McpServer(createServerInfo(), {
|
|
26
|
+
capabilities: createServerCapabilities(),
|
|
27
|
+
instructions: createServerInstructions(config.server.version),
|
|
20
28
|
});
|
|
21
29
|
registerTools(server);
|
|
22
|
-
|
|
30
|
+
registerCachedContentResource(server);
|
|
23
31
|
return server;
|
|
24
32
|
}
|
|
25
|
-
|
|
26
|
-
const server = createMcpServer();
|
|
27
|
-
const transport = new StdioServerTransport();
|
|
33
|
+
function attachServerErrorHandler(server) {
|
|
28
34
|
server.server.onerror = (error) => {
|
|
29
35
|
logError('[MCP Error]', error instanceof Error ? error : { error });
|
|
30
36
|
};
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
}
|
|
38
|
+
function handleShutdownSignal(server, signal) {
|
|
39
|
+
process.stderr.write(`\n${signal} received, shutting down superFetch MCP server...\n`);
|
|
40
|
+
destroyAgents();
|
|
41
|
+
server
|
|
42
|
+
.close()
|
|
43
|
+
.catch((err) => {
|
|
44
|
+
logError('Error during shutdown', err instanceof Error ? err : undefined);
|
|
45
|
+
})
|
|
46
|
+
.finally(() => {
|
|
47
|
+
process.exit(0);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function createShutdownHandler(server) {
|
|
51
|
+
return (signal) => {
|
|
52
|
+
handleShutdownSignal(server, signal);
|
|
43
53
|
};
|
|
54
|
+
}
|
|
55
|
+
function registerSignalHandlers(handler) {
|
|
44
56
|
process.on('SIGINT', () => {
|
|
45
|
-
|
|
57
|
+
handler('SIGINT');
|
|
46
58
|
});
|
|
47
59
|
process.on('SIGTERM', () => {
|
|
48
|
-
|
|
60
|
+
handler('SIGTERM');
|
|
49
61
|
});
|
|
62
|
+
}
|
|
63
|
+
async function connectStdioServer(server, transport) {
|
|
50
64
|
try {
|
|
51
65
|
await server.connect(transport);
|
|
52
66
|
logInfo('superFetch MCP server running on stdio');
|
|
@@ -56,3 +70,10 @@ export async function startStdioServer() {
|
|
|
56
70
|
process.exit(1);
|
|
57
71
|
}
|
|
58
72
|
}
|
|
73
|
+
export async function startStdioServer() {
|
|
74
|
+
const server = createMcpServer();
|
|
75
|
+
const transport = new StdioServerTransport();
|
|
76
|
+
attachServerErrorHandler(server);
|
|
77
|
+
registerSignalHandlers(createShutdownHandler(server));
|
|
78
|
+
await connectStdioServer(server, transport);
|
|
79
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CacheKeyParts } from './cache-keys.js';
|
|
2
|
+
export interface CacheUpdateEvent extends CacheKeyParts {
|
|
3
|
+
cacheKey: string;
|
|
4
|
+
}
|
|
5
|
+
type CacheUpdateListener = (event: CacheUpdateEvent) => void;
|
|
6
|
+
export declare function onCacheUpdate(listener: CacheUpdateListener): () => void;
|
|
7
|
+
export declare function notifyCacheUpdate(cacheKey: string): void;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { parseCacheKey } from './cache-keys.js';
|
|
2
|
+
const updateListeners = new Set();
|
|
3
|
+
export function onCacheUpdate(listener) {
|
|
4
|
+
updateListeners.add(listener);
|
|
5
|
+
return () => {
|
|
6
|
+
updateListeners.delete(listener);
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export function notifyCacheUpdate(cacheKey) {
|
|
10
|
+
if (updateListeners.size === 0)
|
|
11
|
+
return;
|
|
12
|
+
const parts = parseCacheKey(cacheKey);
|
|
13
|
+
if (!parts)
|
|
14
|
+
return;
|
|
15
|
+
const event = { cacheKey, ...parts };
|
|
16
|
+
for (const listener of updateListeners) {
|
|
17
|
+
listener(event);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface CacheKeyParts {
|
|
2
|
+
namespace: string;
|
|
3
|
+
urlHash: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function createCacheKey(namespace: string, url: string, vary?: Record<string, unknown> | string): string | null;
|
|
6
|
+
export declare function parseCacheKey(cacheKey: string): CacheKeyParts | null;
|
|
7
|
+
export declare function toResourceUri(cacheKey: string): string | null;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { CACHE_HASH } from '../config/constants.js';
|
|
2
|
+
import { sha256Hex } from '../utils/crypto.js';
|
|
3
|
+
import { isRecord } from '../utils/guards.js';
|
|
4
|
+
function stableStringify(value) {
|
|
5
|
+
if (!isRecord(value)) {
|
|
6
|
+
if (value === null || value === undefined) {
|
|
7
|
+
return '';
|
|
8
|
+
}
|
|
9
|
+
return JSON.stringify(value);
|
|
10
|
+
}
|
|
11
|
+
if (Array.isArray(value)) {
|
|
12
|
+
return `[${value.map((item) => stableStringify(item)).join(',')}]`;
|
|
13
|
+
}
|
|
14
|
+
const entries = Object.entries(value)
|
|
15
|
+
.filter(([, entryValue]) => entryValue !== undefined)
|
|
16
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
17
|
+
.map(([key, entryValue]) => `${JSON.stringify(key)}:${stableStringify(entryValue)}`);
|
|
18
|
+
return `{${entries.join(',')}}`;
|
|
19
|
+
}
|
|
20
|
+
function createHashFragment(input, length) {
|
|
21
|
+
return sha256Hex(input).substring(0, length);
|
|
22
|
+
}
|
|
23
|
+
function buildCacheKey(namespace, urlHash, varyHash) {
|
|
24
|
+
return varyHash
|
|
25
|
+
? `${namespace}:${urlHash}.${varyHash}`
|
|
26
|
+
: `${namespace}:${urlHash}`;
|
|
27
|
+
}
|
|
28
|
+
function getVaryHash(vary) {
|
|
29
|
+
if (!vary)
|
|
30
|
+
return undefined;
|
|
31
|
+
const varyString = typeof vary === 'string' ? vary : stableStringify(vary);
|
|
32
|
+
if (!varyString)
|
|
33
|
+
return undefined;
|
|
34
|
+
return createHashFragment(varyString, CACHE_HASH.VARY_HASH_LENGTH);
|
|
35
|
+
}
|
|
36
|
+
export function createCacheKey(namespace, url, vary) {
|
|
37
|
+
if (!namespace || !url)
|
|
38
|
+
return null;
|
|
39
|
+
const urlHash = createHashFragment(url, CACHE_HASH.URL_HASH_LENGTH);
|
|
40
|
+
const varyHash = getVaryHash(vary);
|
|
41
|
+
return buildCacheKey(namespace, urlHash, varyHash);
|
|
42
|
+
}
|
|
43
|
+
export function parseCacheKey(cacheKey) {
|
|
44
|
+
if (!cacheKey)
|
|
45
|
+
return null;
|
|
46
|
+
const [namespace, ...rest] = cacheKey.split(':');
|
|
47
|
+
const urlHash = rest.join(':');
|
|
48
|
+
if (!namespace || !urlHash)
|
|
49
|
+
return null;
|
|
50
|
+
return { namespace, urlHash };
|
|
51
|
+
}
|
|
52
|
+
export function toResourceUri(cacheKey) {
|
|
53
|
+
const parts = parseCacheKey(cacheKey);
|
|
54
|
+
if (!parts)
|
|
55
|
+
return null;
|
|
56
|
+
return `superfetch://cache/${parts.namespace}/${parts.urlHash}`;
|
|
57
|
+
}
|