@j0hanz/superfetch 1.2.5 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/README.md +131 -156
  2. package/dist/config/auth-config.d.ts +16 -0
  3. package/dist/config/auth-config.js +53 -0
  4. package/dist/config/constants.d.ts +11 -13
  5. package/dist/config/constants.js +1 -3
  6. package/dist/config/env-parsers.d.ts +7 -0
  7. package/dist/config/env-parsers.js +84 -0
  8. package/dist/config/formatting.d.ts +2 -2
  9. package/dist/config/index.d.ts +47 -53
  10. package/dist/config/index.js +35 -64
  11. package/dist/config/types/content.d.ts +1 -49
  12. package/dist/config/types/runtime.d.ts +8 -16
  13. package/dist/config/types/tools.d.ts +2 -28
  14. package/dist/http/accept-policy.d.ts +3 -0
  15. package/dist/http/accept-policy.js +45 -0
  16. package/dist/http/async-handler.d.ts +2 -0
  17. package/dist/http/async-handler.js +5 -0
  18. package/dist/http/auth-introspection.d.ts +2 -0
  19. package/dist/http/auth-introspection.js +141 -0
  20. package/dist/http/auth-static.d.ts +2 -0
  21. package/dist/http/auth-static.js +23 -0
  22. package/dist/http/auth.d.ts +3 -2
  23. package/dist/http/auth.js +254 -23
  24. package/dist/http/cors.d.ts +6 -6
  25. package/dist/http/cors.js +7 -42
  26. package/dist/http/download-routes.d.ts +0 -12
  27. package/dist/http/download-routes.js +21 -58
  28. package/dist/http/host-allowlist.d.ts +3 -0
  29. package/dist/http/host-allowlist.js +117 -0
  30. package/dist/http/jsonrpc-http.d.ts +2 -0
  31. package/dist/http/jsonrpc-http.js +10 -0
  32. package/dist/http/mcp-routes.d.ts +8 -3
  33. package/dist/http/mcp-routes.js +137 -31
  34. package/dist/http/mcp-session-eviction.d.ts +3 -0
  35. package/dist/http/mcp-session-eviction.js +24 -0
  36. package/dist/http/mcp-session-helpers.d.ts +0 -1
  37. package/dist/http/mcp-session-helpers.js +1 -1
  38. package/dist/http/mcp-session-init.d.ts +7 -0
  39. package/dist/http/mcp-session-init.js +94 -0
  40. package/dist/http/mcp-session-slots.d.ts +17 -0
  41. package/dist/http/mcp-session-slots.js +55 -0
  42. package/dist/http/mcp-session-transport-init.d.ts +7 -0
  43. package/dist/http/mcp-session-transport-init.js +41 -0
  44. package/dist/http/mcp-session-transport.d.ts +7 -0
  45. package/dist/http/mcp-session-transport.js +57 -0
  46. package/dist/http/mcp-session-types.d.ts +5 -0
  47. package/dist/http/mcp-session-types.js +1 -0
  48. package/dist/http/mcp-session.d.ts +9 -9
  49. package/dist/http/mcp-session.js +15 -137
  50. package/dist/http/mcp-sessions.d.ts +43 -0
  51. package/dist/http/mcp-sessions.js +392 -0
  52. package/dist/http/mcp-validation.d.ts +1 -0
  53. package/dist/http/mcp-validation.js +11 -10
  54. package/dist/http/protocol-policy.d.ts +2 -0
  55. package/dist/http/protocol-policy.js +31 -0
  56. package/dist/http/rate-limit.js +7 -4
  57. package/dist/http/server-config.d.ts +1 -0
  58. package/dist/http/server-config.js +40 -0
  59. package/dist/http/server-middleware.d.ts +7 -9
  60. package/dist/http/server-middleware.js +9 -70
  61. package/dist/http/server-shutdown.d.ts +4 -0
  62. package/dist/http/server-shutdown.js +43 -0
  63. package/dist/http/server.d.ts +10 -0
  64. package/dist/http/server.js +546 -61
  65. package/dist/http/session-cleanup.js +8 -5
  66. package/dist/middleware/error-handler.d.ts +1 -1
  67. package/dist/middleware/error-handler.js +32 -33
  68. package/dist/resources/cached-content-params.d.ts +5 -0
  69. package/dist/resources/cached-content-params.js +36 -0
  70. package/dist/resources/cached-content.js +67 -125
  71. package/dist/resources/index.js +0 -82
  72. package/dist/server.js +50 -29
  73. package/dist/services/cache-events.d.ts +8 -0
  74. package/dist/services/cache-events.js +19 -0
  75. package/dist/services/cache-keys.d.ts +7 -0
  76. package/dist/services/cache-keys.js +57 -0
  77. package/dist/services/cache.d.ts +4 -9
  78. package/dist/services/cache.js +77 -139
  79. package/dist/services/context.d.ts +0 -1
  80. package/dist/services/context.js +0 -7
  81. package/dist/services/extractor.js +55 -116
  82. package/dist/services/fetcher/agents.d.ts +2 -2
  83. package/dist/services/fetcher/agents.js +35 -96
  84. package/dist/services/fetcher/dns-selection.d.ts +2 -0
  85. package/dist/services/fetcher/dns-selection.js +72 -0
  86. package/dist/services/fetcher/interceptors.d.ts +0 -22
  87. package/dist/services/fetcher/interceptors.js +18 -32
  88. package/dist/services/fetcher/redirects.js +16 -7
  89. package/dist/services/fetcher/response.js +79 -34
  90. package/dist/services/fetcher.d.ts +22 -3
  91. package/dist/services/fetcher.js +544 -44
  92. package/dist/services/fifo-queue.d.ts +8 -0
  93. package/dist/services/fifo-queue.js +25 -0
  94. package/dist/services/logger.js +2 -2
  95. package/dist/services/metadata-collector.d.ts +1 -9
  96. package/dist/services/metadata-collector.js +71 -2
  97. package/dist/services/transform-worker-pool.d.ts +4 -14
  98. package/dist/services/transform-worker-pool.js +177 -129
  99. package/dist/services/transform-worker-types.d.ts +32 -0
  100. package/dist/services/transform-worker-types.js +14 -0
  101. package/dist/tools/handlers/fetch-markdown.tool.d.ts +3 -4
  102. package/dist/tools/handlers/fetch-markdown.tool.js +20 -72
  103. package/dist/tools/handlers/fetch-single.shared.d.ts +11 -22
  104. package/dist/tools/handlers/fetch-single.shared.js +175 -89
  105. package/dist/tools/handlers/fetch-url.tool.d.ts +7 -1
  106. package/dist/tools/handlers/fetch-url.tool.js +84 -119
  107. package/dist/tools/index.js +21 -40
  108. package/dist/tools/schemas.d.ts +1 -51
  109. package/dist/tools/schemas.js +1 -107
  110. package/dist/tools/utils/cached-markdown.d.ts +5 -0
  111. package/dist/tools/utils/cached-markdown.js +46 -0
  112. package/dist/tools/utils/content-shaping.d.ts +4 -0
  113. package/dist/tools/utils/content-shaping.js +67 -0
  114. package/dist/tools/utils/content-transform.d.ts +5 -17
  115. package/dist/tools/utils/content-transform.js +134 -114
  116. package/dist/tools/utils/fetch-pipeline.d.ts +0 -8
  117. package/dist/tools/utils/fetch-pipeline.js +57 -63
  118. package/dist/tools/utils/frontmatter.d.ts +3 -0
  119. package/dist/tools/utils/frontmatter.js +73 -0
  120. package/dist/tools/utils/inline-content.d.ts +1 -2
  121. package/dist/tools/utils/inline-content.js +4 -7
  122. package/dist/tools/utils/markdown-heuristics.d.ts +1 -0
  123. package/dist/tools/utils/markdown-heuristics.js +19 -0
  124. package/dist/tools/utils/markdown-signals.d.ts +1 -0
  125. package/dist/tools/utils/markdown-signals.js +19 -0
  126. package/dist/tools/utils/raw-markdown-frontmatter.d.ts +3 -0
  127. package/dist/tools/utils/raw-markdown-frontmatter.js +73 -0
  128. package/dist/tools/utils/raw-markdown.d.ts +6 -0
  129. package/dist/tools/utils/raw-markdown.js +135 -0
  130. package/dist/transformers/markdown/fenced-code-rule.d.ts +2 -0
  131. package/dist/transformers/markdown/fenced-code-rule.js +38 -0
  132. package/dist/transformers/markdown/frontmatter.d.ts +2 -0
  133. package/dist/transformers/markdown/frontmatter.js +45 -0
  134. package/dist/transformers/markdown/noise-rule.d.ts +2 -0
  135. package/dist/transformers/markdown/noise-rule.js +80 -0
  136. package/dist/transformers/markdown/turndown-instance.d.ts +2 -0
  137. package/dist/transformers/markdown/turndown-instance.js +19 -0
  138. package/dist/transformers/markdown.d.ts +2 -0
  139. package/dist/transformers/markdown.js +185 -0
  140. package/dist/transformers/markdown.transformer.js +5 -117
  141. package/dist/utils/cached-payload.d.ts +7 -0
  142. package/dist/utils/cached-payload.js +36 -0
  143. package/dist/utils/code-language-bash.d.ts +1 -0
  144. package/dist/utils/code-language-bash.js +48 -0
  145. package/dist/utils/code-language-core.d.ts +2 -0
  146. package/dist/utils/code-language-core.js +13 -0
  147. package/dist/utils/code-language-detectors.d.ts +5 -0
  148. package/dist/utils/code-language-detectors.js +142 -0
  149. package/dist/utils/code-language-helpers.d.ts +5 -0
  150. package/dist/utils/code-language-helpers.js +62 -0
  151. package/dist/utils/code-language-parsing.d.ts +5 -0
  152. package/dist/utils/code-language-parsing.js +62 -0
  153. package/dist/utils/code-language.d.ts +9 -0
  154. package/dist/utils/code-language.js +250 -46
  155. package/dist/utils/error-details.d.ts +3 -0
  156. package/dist/utils/error-details.js +12 -0
  157. package/dist/utils/error-utils.js +1 -1
  158. package/dist/utils/filename-generator.js +34 -12
  159. package/dist/utils/guards.d.ts +1 -0
  160. package/dist/utils/guards.js +3 -0
  161. package/dist/utils/header-normalizer.d.ts +0 -3
  162. package/dist/utils/header-normalizer.js +3 -3
  163. package/dist/utils/ip-address.d.ts +4 -0
  164. package/dist/utils/ip-address.js +6 -0
  165. package/dist/utils/tool-error-handler.d.ts +2 -2
  166. package/dist/utils/tool-error-handler.js +14 -46
  167. package/dist/utils/url-transformer.d.ts +7 -0
  168. package/dist/utils/url-transformer.js +147 -0
  169. package/dist/utils/url-validator.d.ts +1 -2
  170. package/dist/utils/url-validator.js +53 -114
  171. package/dist/workers/content-transform.worker.d.ts +1 -0
  172. package/dist/workers/content-transform.worker.js +40 -0
  173. package/package.json +17 -18
@@ -1,57 +1,56 @@
1
1
  import { FetchError } from '../errors/app-error.js';
2
2
  import { logError } from '../services/logger.js';
3
- function getStatusCode(err) {
4
- return err instanceof FetchError ? err.statusCode : 500;
3
+ function getStatusCode(fetchError) {
4
+ return fetchError ? fetchError.statusCode : 500;
5
5
  }
6
- function getErrorCode(err) {
7
- return err instanceof FetchError ? err.code : 'INTERNAL_ERROR';
6
+ function getErrorCode(fetchError) {
7
+ return fetchError ? fetchError.code : 'INTERNAL_ERROR';
8
8
  }
9
- function getErrorMessage(err) {
10
- return err instanceof FetchError ? err.message : 'Internal Server Error';
9
+ function getErrorMessage(fetchError) {
10
+ return fetchError ? fetchError.message : 'Internal Server Error';
11
11
  }
12
- function getErrorDetails(err) {
13
- if (err instanceof FetchError && Object.keys(err.details).length > 0) {
14
- return err.details;
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, err) {
19
- const retryAfter = resolveRetryAfter(err);
20
- if (!retryAfter)
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(err) {
25
- const details = getErrorDetails(err);
24
+ function buildErrorResponse(fetchError) {
25
+ const details = getErrorDetails(fetchError);
26
26
  const response = {
27
27
  error: {
28
- message: getErrorMessage(err),
29
- code: getErrorCode(err),
30
- statusCode: getStatusCode(err),
28
+ message: getErrorMessage(fetchError),
29
+ code: getErrorCode(fetchError),
30
+ statusCode: getStatusCode(fetchError),
31
31
  ...(details && { details }),
32
32
  },
33
33
  };
34
- if (process.env.NODE_ENV === 'development' && err.stack) {
35
- response.error.stack = err.stack;
36
- }
34
+ // Never expose stack traces in production
37
35
  return response;
38
36
  }
39
- function resolveRetryAfter(err) {
40
- if (!(err instanceof FetchError))
41
- return null;
42
- if (err.statusCode !== 429)
43
- return null;
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, _next) {
53
- const statusCode = getStatusCode(err);
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, err);
56
- res.status(statusCode).json(buildErrorResponse(err));
54
+ setRetryAfterHeader(res, fetchError);
55
+ res.status(statusCode).json(buildErrorResponse(fetchError));
57
56
  }
@@ -0,0 +1,5 @@
1
+ export declare const CACHE_NAMESPACE = "markdown";
2
+ export declare function resolveCacheParams(params: unknown): {
3
+ namespace: string;
4
+ urlHash: string;
5
+ };
@@ -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 { getErrorMessage } from '../utils/error-utils.js';
6
- const VALID_NAMESPACES = new Set(['url', 'markdown', 'links']);
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: resolveCacheMimeType(namespace),
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 = cache.parseCacheKey(key);
21
- return parts ? buildResourceEntry(parts.namespace, parts.urlHash) : null;
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 resolveCacheParams(params) {
59
- const namespace = resolveStringParam(params.namespace);
60
- const urlHash = resolveStringParam(params.urlHash);
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: url, links, markdown. UrlHash: SHA-256 hash of the URL.',
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, namespace);
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 = cache.toResourceUri(cacheKey);
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 resolveCacheMimeType(namespace) {
148
- if (namespace === 'markdown')
149
- return 'text/markdown';
150
- if (namespace === 'url')
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 isCachedPayload(value) {
173
- if (!value || typeof value !== 'object')
174
- return false;
175
- const record = value;
176
- return ((record.content === undefined || typeof record.content === 'string') &&
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 typeof payload.content === 'string' ? payload.content : null;
123
+ return {
124
+ contents: [
125
+ {
126
+ uri: uri.href,
127
+ mimeType: 'text/markdown',
128
+ text: resolvedContent,
129
+ },
130
+ ],
131
+ };
190
132
  }
@@ -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 { registerResources } from './resources/index.js';
9
- export function createMcpServer() {
10
- const server = new McpServer({
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
- capabilities: {
15
- tools: { listChanged: false },
16
- resources: { listChanged: true, subscribe: true },
17
- logging: {},
18
- },
19
- instructions: `superFetch MCP server v${config.server.version} - AI-optimized web content fetching with JSONL/Markdown output. Provides tools for fetching, parsing, and transforming web content into structured formats suitable for LLM consumption. Supports resource subscriptions for cache updates.`,
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
- registerResources(server);
30
+ registerCachedContentResource(server);
23
31
  return server;
24
32
  }
25
- export async function startStdioServer() {
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
- const handleShutdown = (signal) => {
32
- process.stderr.write(`\n${signal} received, shutting down superFetch MCP server...\n`);
33
- destroyAgents();
34
- destroyTransformWorkers();
35
- server
36
- .close()
37
- .catch((err) => {
38
- logError('Error during shutdown', err instanceof Error ? err : undefined);
39
- })
40
- .finally(() => {
41
- process.exit(0);
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
- handleShutdown('SIGINT');
57
+ handler('SIGINT');
46
58
  });
47
59
  process.on('SIGTERM', () => {
48
- handleShutdown('SIGTERM');
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
+ }