@j0hanz/fetch-url-mcp 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/dist/cli.d.ts +2 -3
  2. package/dist/cli.js +1 -2
  3. package/dist/http/auth.d.ts +5 -3
  4. package/dist/http/auth.js +64 -15
  5. package/dist/http/health.d.ts +1 -2
  6. package/dist/http/health.js +7 -18
  7. package/dist/http/helpers.d.ts +3 -4
  8. package/dist/http/helpers.js +21 -21
  9. package/dist/http/native.d.ts +0 -1
  10. package/dist/http/native.js +34 -26
  11. package/dist/http/rate-limit.d.ts +0 -1
  12. package/dist/http/rate-limit.js +3 -4
  13. package/dist/index.d.ts +0 -1
  14. package/dist/index.js +17 -18
  15. package/dist/lib/{markdown-cleanup.d.ts → content.d.ts} +4 -2
  16. package/dist/lib/content.js +1356 -0
  17. package/dist/lib/core.d.ts +253 -0
  18. package/dist/lib/core.js +1228 -0
  19. package/dist/lib/{tool-pipeline.d.ts → fetch-pipeline.d.ts} +1 -2
  20. package/dist/lib/{tool-pipeline.js → fetch-pipeline.js} +10 -19
  21. package/dist/lib/{fetch.d.ts → http.d.ts} +7 -9
  22. package/dist/lib/{fetch.js → http.js} +706 -944
  23. package/dist/lib/mcp-tools.d.ts +28 -0
  24. package/dist/lib/mcp-tools.js +107 -0
  25. package/dist/lib/{tool-progress.d.ts → progress.d.ts} +0 -1
  26. package/dist/lib/{tool-progress.js → progress.js} +8 -13
  27. package/dist/lib/task-handlers.d.ts +5 -0
  28. package/dist/lib/{mcp.js → task-handlers.js} +56 -12
  29. package/dist/lib/url.d.ts +70 -0
  30. package/dist/lib/url.js +686 -0
  31. package/dist/lib/utils.d.ts +58 -0
  32. package/dist/lib/utils.js +304 -0
  33. package/dist/prompts/index.d.ts +0 -1
  34. package/dist/prompts/index.js +0 -1
  35. package/dist/resources/index.d.ts +0 -1
  36. package/dist/resources/index.js +74 -33
  37. package/dist/resources/instructions.d.ts +0 -1
  38. package/dist/resources/instructions.js +2 -2
  39. package/dist/schemas/inputs.d.ts +0 -1
  40. package/dist/schemas/inputs.js +2 -3
  41. package/dist/schemas/outputs.d.ts +0 -1
  42. package/dist/schemas/outputs.js +1 -2
  43. package/dist/server.d.ts +0 -1
  44. package/dist/server.js +16 -26
  45. package/dist/tasks/execution.d.ts +0 -1
  46. package/dist/tasks/execution.js +27 -24
  47. package/dist/tasks/manager.d.ts +7 -3
  48. package/dist/tasks/manager.js +53 -34
  49. package/dist/tasks/owner.d.ts +1 -2
  50. package/dist/tasks/owner.js +1 -2
  51. package/dist/tasks/tool-registry.d.ts +1 -2
  52. package/dist/tasks/tool-registry.js +0 -1
  53. package/dist/tools/fetch-url.d.ts +1 -2
  54. package/dist/tools/fetch-url.js +39 -31
  55. package/dist/tools/index.d.ts +0 -1
  56. package/dist/tools/index.js +0 -1
  57. package/dist/transform/html-translators.d.ts +1 -0
  58. package/dist/transform/html-translators.js +454 -0
  59. package/dist/transform/metadata.d.ts +4 -0
  60. package/dist/transform/metadata.js +183 -0
  61. package/dist/transform/transform.d.ts +0 -1
  62. package/dist/transform/transform.js +24 -641
  63. package/dist/transform/types.d.ts +9 -11
  64. package/dist/transform/types.js +0 -1
  65. package/dist/transform/worker-pool.d.ts +0 -1
  66. package/dist/transform/worker-pool.js +7 -16
  67. package/dist/transform/workers/shared.d.ts +0 -1
  68. package/dist/transform/workers/shared.js +1 -2
  69. package/dist/transform/workers/transform-child.d.ts +0 -1
  70. package/dist/transform/workers/transform-child.js +0 -1
  71. package/dist/transform/workers/transform-worker.d.ts +0 -1
  72. package/dist/transform/workers/transform-worker.js +0 -1
  73. package/package.json +6 -3
  74. package/dist/cli.d.ts.map +0 -1
  75. package/dist/cli.js.map +0 -1
  76. package/dist/http/auth.d.ts.map +0 -1
  77. package/dist/http/auth.js.map +0 -1
  78. package/dist/http/health.d.ts.map +0 -1
  79. package/dist/http/health.js.map +0 -1
  80. package/dist/http/helpers.d.ts.map +0 -1
  81. package/dist/http/helpers.js.map +0 -1
  82. package/dist/http/native.d.ts.map +0 -1
  83. package/dist/http/native.js.map +0 -1
  84. package/dist/http/rate-limit.d.ts.map +0 -1
  85. package/dist/http/rate-limit.js.map +0 -1
  86. package/dist/index.d.ts.map +0 -1
  87. package/dist/index.js.map +0 -1
  88. package/dist/lib/cache.d.ts +0 -54
  89. package/dist/lib/cache.d.ts.map +0 -1
  90. package/dist/lib/cache.js +0 -264
  91. package/dist/lib/cache.js.map +0 -1
  92. package/dist/lib/config.d.ts +0 -143
  93. package/dist/lib/config.d.ts.map +0 -1
  94. package/dist/lib/config.js +0 -476
  95. package/dist/lib/config.js.map +0 -1
  96. package/dist/lib/crypto.d.ts +0 -4
  97. package/dist/lib/crypto.d.ts.map +0 -1
  98. package/dist/lib/crypto.js +0 -56
  99. package/dist/lib/crypto.js.map +0 -1
  100. package/dist/lib/dom-noise-removal.d.ts +0 -2
  101. package/dist/lib/dom-noise-removal.d.ts.map +0 -1
  102. package/dist/lib/dom-noise-removal.js +0 -494
  103. package/dist/lib/dom-noise-removal.js.map +0 -1
  104. package/dist/lib/download.d.ts +0 -4
  105. package/dist/lib/download.d.ts.map +0 -1
  106. package/dist/lib/download.js +0 -106
  107. package/dist/lib/download.js.map +0 -1
  108. package/dist/lib/errors.d.ts +0 -14
  109. package/dist/lib/errors.d.ts.map +0 -1
  110. package/dist/lib/errors.js +0 -72
  111. package/dist/lib/errors.js.map +0 -1
  112. package/dist/lib/fetch-content.d.ts +0 -5
  113. package/dist/lib/fetch-content.d.ts.map +0 -1
  114. package/dist/lib/fetch-content.js +0 -164
  115. package/dist/lib/fetch-content.js.map +0 -1
  116. package/dist/lib/fetch-stream.d.ts +0 -5
  117. package/dist/lib/fetch-stream.d.ts.map +0 -1
  118. package/dist/lib/fetch-stream.js +0 -29
  119. package/dist/lib/fetch-stream.js.map +0 -1
  120. package/dist/lib/fetch.d.ts.map +0 -1
  121. package/dist/lib/fetch.js.map +0 -1
  122. package/dist/lib/host-normalization.d.ts +0 -2
  123. package/dist/lib/host-normalization.d.ts.map +0 -1
  124. package/dist/lib/host-normalization.js +0 -91
  125. package/dist/lib/host-normalization.js.map +0 -1
  126. package/dist/lib/ip-blocklist.d.ts +0 -9
  127. package/dist/lib/ip-blocklist.d.ts.map +0 -1
  128. package/dist/lib/ip-blocklist.js +0 -79
  129. package/dist/lib/ip-blocklist.js.map +0 -1
  130. package/dist/lib/json.d.ts +0 -2
  131. package/dist/lib/json.d.ts.map +0 -1
  132. package/dist/lib/json.js +0 -45
  133. package/dist/lib/json.js.map +0 -1
  134. package/dist/lib/language-detection.d.ts +0 -3
  135. package/dist/lib/language-detection.d.ts.map +0 -1
  136. package/dist/lib/language-detection.js +0 -355
  137. package/dist/lib/language-detection.js.map +0 -1
  138. package/dist/lib/markdown-cleanup.d.ts.map +0 -1
  139. package/dist/lib/markdown-cleanup.js +0 -532
  140. package/dist/lib/markdown-cleanup.js.map +0 -1
  141. package/dist/lib/mcp-lifecycle.d.ts +0 -5
  142. package/dist/lib/mcp-lifecycle.d.ts.map +0 -1
  143. package/dist/lib/mcp-lifecycle.js +0 -51
  144. package/dist/lib/mcp-lifecycle.js.map +0 -1
  145. package/dist/lib/mcp-validator.d.ts +0 -17
  146. package/dist/lib/mcp-validator.d.ts.map +0 -1
  147. package/dist/lib/mcp-validator.js +0 -45
  148. package/dist/lib/mcp-validator.js.map +0 -1
  149. package/dist/lib/mcp.d.ts +0 -4
  150. package/dist/lib/mcp.d.ts.map +0 -1
  151. package/dist/lib/mcp.js.map +0 -1
  152. package/dist/lib/observability.d.ts +0 -23
  153. package/dist/lib/observability.d.ts.map +0 -1
  154. package/dist/lib/observability.js +0 -238
  155. package/dist/lib/observability.js.map +0 -1
  156. package/dist/lib/server-tuning.d.ts +0 -15
  157. package/dist/lib/server-tuning.d.ts.map +0 -1
  158. package/dist/lib/server-tuning.js +0 -49
  159. package/dist/lib/server-tuning.js.map +0 -1
  160. package/dist/lib/session.d.ts +0 -45
  161. package/dist/lib/session.d.ts.map +0 -1
  162. package/dist/lib/session.js +0 -263
  163. package/dist/lib/session.js.map +0 -1
  164. package/dist/lib/timer-utils.d.ts +0 -13
  165. package/dist/lib/timer-utils.d.ts.map +0 -1
  166. package/dist/lib/timer-utils.js +0 -44
  167. package/dist/lib/timer-utils.js.map +0 -1
  168. package/dist/lib/tool-errors.d.ts +0 -12
  169. package/dist/lib/tool-errors.d.ts.map +0 -1
  170. package/dist/lib/tool-errors.js +0 -55
  171. package/dist/lib/tool-errors.js.map +0 -1
  172. package/dist/lib/tool-pipeline.d.ts.map +0 -1
  173. package/dist/lib/tool-pipeline.js.map +0 -1
  174. package/dist/lib/tool-progress.d.ts.map +0 -1
  175. package/dist/lib/tool-progress.js.map +0 -1
  176. package/dist/lib/type-guards.d.ts +0 -16
  177. package/dist/lib/type-guards.d.ts.map +0 -1
  178. package/dist/lib/type-guards.js +0 -13
  179. package/dist/lib/type-guards.js.map +0 -1
  180. package/dist/prompts/index.d.ts.map +0 -1
  181. package/dist/prompts/index.js.map +0 -1
  182. package/dist/resources/index.d.ts.map +0 -1
  183. package/dist/resources/index.js.map +0 -1
  184. package/dist/resources/instructions.d.ts.map +0 -1
  185. package/dist/resources/instructions.js.map +0 -1
  186. package/dist/schemas/inputs.d.ts.map +0 -1
  187. package/dist/schemas/inputs.js.map +0 -1
  188. package/dist/schemas/outputs.d.ts.map +0 -1
  189. package/dist/schemas/outputs.js.map +0 -1
  190. package/dist/server.d.ts.map +0 -1
  191. package/dist/server.js.map +0 -1
  192. package/dist/tasks/execution.d.ts.map +0 -1
  193. package/dist/tasks/execution.js.map +0 -1
  194. package/dist/tasks/manager.d.ts.map +0 -1
  195. package/dist/tasks/manager.js.map +0 -1
  196. package/dist/tasks/owner.d.ts.map +0 -1
  197. package/dist/tasks/owner.js.map +0 -1
  198. package/dist/tasks/tool-registry.d.ts.map +0 -1
  199. package/dist/tasks/tool-registry.js.map +0 -1
  200. package/dist/tools/fetch-url.d.ts.map +0 -1
  201. package/dist/tools/fetch-url.js.map +0 -1
  202. package/dist/tools/index.d.ts.map +0 -1
  203. package/dist/tools/index.js.map +0 -1
  204. package/dist/transform/transform.d.ts.map +0 -1
  205. package/dist/transform/transform.js.map +0 -1
  206. package/dist/transform/types.d.ts.map +0 -1
  207. package/dist/transform/types.js.map +0 -1
  208. package/dist/transform/worker-pool.d.ts.map +0 -1
  209. package/dist/transform/worker-pool.js.map +0 -1
  210. package/dist/transform/workers/shared.d.ts.map +0 -1
  211. package/dist/transform/workers/shared.js.map +0 -1
  212. package/dist/transform/workers/transform-child.d.ts.map +0 -1
  213. package/dist/transform/workers/transform-child.js.map +0 -1
  214. package/dist/transform/workers/transform-worker.d.ts.map +0 -1
  215. package/dist/transform/workers/transform-worker.js.map +0 -1
@@ -0,0 +1,1228 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { EventEmitter } from 'node:events';
3
+ import { accessSync, constants as fsConstants, readFileSync } from 'node:fs';
4
+ import { findPackageJSON } from 'node:module';
5
+ import { isIP } from 'node:net';
6
+ import process from 'node:process';
7
+ import { domainToASCII } from 'node:url';
8
+ import { inspect, stripVTControlCharacters } from 'node:util';
9
+ import {} from '@modelcontextprotocol/sdk/server/mcp.js';
10
+ import {} from '@modelcontextprotocol/sdk/server/streamableHttp.js';
11
+ import { z } from 'zod';
12
+ import { getErrorMessage, isAbortError, sha256Hex, stableStringify as stableJsonStringify, startAbortableIntervalLoop, } from './utils.js';
13
+ export const serverVersion = readServerVersion(import.meta.url);
14
+ const LOG_LEVELS = ['debug', 'info', 'warn', 'error'];
15
+ const ALLOWED_LOG_LEVELS = new Set(LOG_LEVELS);
16
+ const DEFAULT_HEADING_KEYWORDS = [
17
+ 'overview',
18
+ 'introduction',
19
+ 'summary',
20
+ 'conclusion',
21
+ 'prerequisites',
22
+ 'requirements',
23
+ 'installation',
24
+ 'configuration',
25
+ 'usage',
26
+ 'features',
27
+ 'limitations',
28
+ 'troubleshooting',
29
+ 'faq',
30
+ 'resources',
31
+ 'references',
32
+ 'changelog',
33
+ 'license',
34
+ 'acknowledgments',
35
+ 'appendix',
36
+ ];
37
+ class ConfigError extends Error {
38
+ name = 'ConfigError';
39
+ }
40
+ function isMissingEnvFileError(error) {
41
+ if (!error || typeof error !== 'object')
42
+ return false;
43
+ const { code } = error;
44
+ return code === 'ENOENT' || code === 'ERR_ENV_FILE_NOT_FOUND';
45
+ }
46
+ function loadEnvFileIfAvailable() {
47
+ if (typeof process.loadEnvFile !== 'function')
48
+ return;
49
+ try {
50
+ process.loadEnvFile();
51
+ }
52
+ catch (error) {
53
+ if (isMissingEnvFileError(error))
54
+ return;
55
+ throw error;
56
+ }
57
+ }
58
+ loadEnvFileIfAvailable();
59
+ const { env } = process;
60
+ function buildIpv4(parts) {
61
+ return parts.join('.');
62
+ }
63
+ function stripTrailingDots(value) {
64
+ let result = value;
65
+ while (result.endsWith('.'))
66
+ result = result.slice(0, -1);
67
+ return result;
68
+ }
69
+ function formatHostForUrl(hostname) {
70
+ if (hostname.includes(':') && !hostname.startsWith('['))
71
+ return `[${hostname}]`;
72
+ return hostname;
73
+ }
74
+ function normalizeHostname(value) {
75
+ const trimmed = value.trim();
76
+ if (!trimmed)
77
+ return null;
78
+ const lowered = trimmed.toLowerCase();
79
+ const ipType = isIP(lowered);
80
+ if (ipType)
81
+ return stripTrailingDots(lowered);
82
+ const ascii = domainToASCII(lowered);
83
+ return ascii ? stripTrailingDots(ascii) : null;
84
+ }
85
+ function normalizeHostValue(value) {
86
+ const raw = value.trim();
87
+ if (!raw)
88
+ return null;
89
+ // Full URL
90
+ if (raw.includes('://')) {
91
+ if (!URL.canParse(raw))
92
+ return null;
93
+ return normalizeHostname(new URL(raw).hostname);
94
+ }
95
+ // host[:port]
96
+ const candidateUrl = `http://${raw}`;
97
+ if (URL.canParse(candidateUrl)) {
98
+ return normalizeHostname(new URL(candidateUrl).hostname);
99
+ }
100
+ const lowered = raw.toLowerCase();
101
+ // [::1]:port
102
+ if (lowered.startsWith('[')) {
103
+ const end = lowered.indexOf(']');
104
+ if (end === -1)
105
+ return null;
106
+ return normalizeHostname(lowered.slice(1, end));
107
+ }
108
+ // Bare IPv6
109
+ if (isIP(lowered) === 6)
110
+ return stripTrailingDots(lowered);
111
+ // Split host:port (single colon only)
112
+ const firstColon = lowered.indexOf(':');
113
+ if (firstColon === -1)
114
+ return normalizeHostname(lowered);
115
+ if (lowered.includes(':', firstColon + 1))
116
+ return null;
117
+ const host = lowered.slice(0, firstColon);
118
+ return host ? normalizeHostname(host) : null;
119
+ }
120
+ function parseIntegerValue(envValue, min, max) {
121
+ if (!envValue)
122
+ return null;
123
+ const parsed = Number.parseInt(envValue, 10);
124
+ if (Number.isNaN(parsed))
125
+ return null;
126
+ if (min !== undefined && parsed < min)
127
+ return null;
128
+ if (max !== undefined && parsed > max)
129
+ return null;
130
+ return parsed;
131
+ }
132
+ function parseOptionalInteger(envValue, min, max) {
133
+ return parseIntegerValue(envValue, min, max) ?? undefined;
134
+ }
135
+ function parseInteger(envValue, defaultValue, min, max) {
136
+ return parseIntegerValue(envValue, min, max) ?? defaultValue;
137
+ }
138
+ function parseBoolean(envValue, defaultValue) {
139
+ if (!envValue)
140
+ return defaultValue;
141
+ return envValue.trim().toLowerCase() !== 'false';
142
+ }
143
+ function parseList(envValue) {
144
+ if (!envValue)
145
+ return [];
146
+ return envValue
147
+ .split(/[\s,]+/)
148
+ .map((entry) => entry.trim())
149
+ .filter((entry) => entry.length > 0);
150
+ }
151
+ function parseListOrDefault(envValue, defaultValue) {
152
+ const parsed = parseList(envValue);
153
+ return parsed.length > 0 ? parsed : [...defaultValue];
154
+ }
155
+ function normalizeLocale(value) {
156
+ if (!value)
157
+ return undefined;
158
+ const trimmed = value.trim();
159
+ if (!trimmed)
160
+ return undefined;
161
+ const lowered = trimmed.toLowerCase();
162
+ if (lowered === 'system' || lowered === 'default')
163
+ return undefined;
164
+ return trimmed;
165
+ }
166
+ function isLogLevel(value) {
167
+ return ALLOWED_LOG_LEVELS.has(value);
168
+ }
169
+ function parseLogLevel(envValue) {
170
+ if (!envValue)
171
+ return 'info';
172
+ const level = envValue.toLowerCase();
173
+ return isLogLevel(level) ? level : 'info';
174
+ }
175
+ function parseTransformWorkerMode(envValue) {
176
+ if (!envValue)
177
+ return 'threads';
178
+ const normalized = envValue.trim().toLowerCase();
179
+ if (normalized === 'process' || normalized === 'fork')
180
+ return 'process';
181
+ return 'threads';
182
+ }
183
+ function parsePort(envValue) {
184
+ if (envValue?.trim() === '0')
185
+ return 0;
186
+ return parseInteger(envValue, 3000, 1024, 65535);
187
+ }
188
+ function parseUrlEnv(value, name) {
189
+ if (!value)
190
+ return undefined;
191
+ if (!URL.canParse(value)) {
192
+ throw new ConfigError(`Invalid ${name} value: ${value}`);
193
+ }
194
+ return new URL(value);
195
+ }
196
+ function readUrlEnv(name) {
197
+ return parseUrlEnv(env[name], name);
198
+ }
199
+ function parseAllowedHosts(envValue) {
200
+ const hosts = new Set();
201
+ for (const entry of parseList(envValue)) {
202
+ const normalized = normalizeHostValue(entry);
203
+ if (normalized)
204
+ hosts.add(normalized);
205
+ }
206
+ return hosts;
207
+ }
208
+ function readOptionalFilePath(value) {
209
+ if (!value)
210
+ return undefined;
211
+ const trimmed = value.trim();
212
+ return trimmed.length > 0 ? trimmed : undefined;
213
+ }
214
+ function assertFileReadable(filePath, envVar) {
215
+ try {
216
+ accessSync(filePath, fsConstants.R_OK);
217
+ }
218
+ catch {
219
+ throw new ConfigError(`${envVar} points to "${filePath}" which does not exist or is not readable`);
220
+ }
221
+ }
222
+ const MAX_HTML_BYTES = 10 * 1024 * 1024;
223
+ const MAX_INLINE_CONTENT_CHARS = parseInteger(env['MAX_INLINE_CONTENT_CHARS'], 0, 0, MAX_HTML_BYTES);
224
+ const DEFAULT_SESSION_TTL_MS = 30 * 60 * 1000;
225
+ const DEFAULT_SESSION_INIT_TIMEOUT_MS = 10000;
226
+ const DEFAULT_MAX_SESSIONS = 200;
227
+ const DEFAULT_USER_AGENT = `fetch-url-mcp/${serverVersion}`;
228
+ const DEFAULT_TOOL_TIMEOUT_PADDING_MS = 5000;
229
+ const DEFAULT_TRANSFORM_TIMEOUT_MS = 30000;
230
+ const DEFAULT_FETCH_TIMEOUT_MS = parseInteger(env['FETCH_TIMEOUT_MS'], 15000, 1000, 60000);
231
+ const DEFAULT_TOOL_TIMEOUT_MS = DEFAULT_FETCH_TIMEOUT_MS +
232
+ DEFAULT_TRANSFORM_TIMEOUT_MS +
233
+ DEFAULT_TOOL_TIMEOUT_PADDING_MS;
234
+ const DEFAULT_TASKS_MAX_TOTAL = parseInteger(env['TASKS_MAX_TOTAL'], 5000, 1);
235
+ const DEFAULT_TASKS_MAX_PER_OWNER = parseInteger(env['TASKS_MAX_PER_OWNER'], 1000, 1);
236
+ const RESOLVED_TASKS_MAX_PER_OWNER = Math.min(DEFAULT_TASKS_MAX_PER_OWNER, DEFAULT_TASKS_MAX_TOTAL);
237
+ function resolveWorkerResourceLimits() {
238
+ const limits = {};
239
+ let hasAny = false;
240
+ const entries = [
241
+ [
242
+ 'maxOldGenerationSizeMb',
243
+ parseOptionalInteger(env['TRANSFORM_WORKER_MAX_OLD_GENERATION_MB'], 1),
244
+ ],
245
+ [
246
+ 'maxYoungGenerationSizeMb',
247
+ parseOptionalInteger(env['TRANSFORM_WORKER_MAX_YOUNG_GENERATION_MB'], 1),
248
+ ],
249
+ [
250
+ 'codeRangeSizeMb',
251
+ parseOptionalInteger(env['TRANSFORM_WORKER_CODE_RANGE_MB'], 1),
252
+ ],
253
+ ['stackSizeMb', parseOptionalInteger(env['TRANSFORM_WORKER_STACK_MB'], 1)],
254
+ ];
255
+ for (const [key, value] of entries) {
256
+ if (value === undefined)
257
+ continue;
258
+ limits[key] = value;
259
+ hasAny = true;
260
+ }
261
+ return hasAny ? limits : undefined;
262
+ }
263
+ function readOAuthUrls(baseUrl) {
264
+ const issuerUrl = readUrlEnv('OAUTH_ISSUER_URL');
265
+ const authorizationUrl = readUrlEnv('OAUTH_AUTHORIZATION_URL');
266
+ const tokenUrl = readUrlEnv('OAUTH_TOKEN_URL');
267
+ const revocationUrl = readUrlEnv('OAUTH_REVOCATION_URL');
268
+ const registrationUrl = readUrlEnv('OAUTH_REGISTRATION_URL');
269
+ const introspectionUrl = readUrlEnv('OAUTH_INTROSPECTION_URL');
270
+ const resourceUrl = new URL('/mcp', baseUrl);
271
+ return {
272
+ issuerUrl,
273
+ authorizationUrl,
274
+ tokenUrl,
275
+ revocationUrl,
276
+ registrationUrl,
277
+ introspectionUrl,
278
+ resourceUrl,
279
+ };
280
+ }
281
+ function resolveAuthMode(urls) {
282
+ const oauthConfigured = [
283
+ urls.issuerUrl,
284
+ urls.authorizationUrl,
285
+ urls.tokenUrl,
286
+ urls.introspectionUrl,
287
+ ].some((value) => value !== undefined);
288
+ return oauthConfigured ? 'oauth' : 'static';
289
+ }
290
+ function collectStaticTokens() {
291
+ const staticTokens = new Set(parseList(env['ACCESS_TOKENS']));
292
+ if (env['API_KEY'])
293
+ staticTokens.add(env['API_KEY']);
294
+ return [...staticTokens];
295
+ }
296
+ function buildAuthConfig(baseUrl) {
297
+ const urls = readOAuthUrls(baseUrl);
298
+ const mode = resolveAuthMode(urls);
299
+ return {
300
+ mode,
301
+ ...urls,
302
+ requiredScopes: parseList(env['OAUTH_REQUIRED_SCOPES']),
303
+ clientId: env['OAUTH_CLIENT_ID'],
304
+ clientSecret: env['OAUTH_CLIENT_SECRET'],
305
+ introspectionTimeoutMs: 5000,
306
+ staticTokens: collectStaticTokens(),
307
+ };
308
+ }
309
+ function buildHttpsConfig() {
310
+ const keyFile = readOptionalFilePath(env['SERVER_TLS_KEY_FILE']);
311
+ const certFile = readOptionalFilePath(env['SERVER_TLS_CERT_FILE']);
312
+ const caFile = readOptionalFilePath(env['SERVER_TLS_CA_FILE']);
313
+ if (keyFile)
314
+ assertFileReadable(keyFile, 'SERVER_TLS_KEY_FILE');
315
+ if (certFile)
316
+ assertFileReadable(certFile, 'SERVER_TLS_CERT_FILE');
317
+ if (caFile)
318
+ assertFileReadable(caFile, 'SERVER_TLS_CA_FILE');
319
+ if ((keyFile && !certFile) || (!keyFile && certFile)) {
320
+ throw new ConfigError('Both SERVER_TLS_KEY_FILE and SERVER_TLS_CERT_FILE must be set together');
321
+ }
322
+ return {
323
+ enabled: Boolean(keyFile && certFile),
324
+ keyFile,
325
+ certFile,
326
+ caFile,
327
+ };
328
+ }
329
+ const LOOPBACK_V4 = buildIpv4([127, 0, 0, 1]);
330
+ const ANY_V4 = buildIpv4([0, 0, 0, 0]);
331
+ const METADATA_V4_AWS = buildIpv4([169, 254, 169, 254]);
332
+ const METADATA_V4_AZURE = buildIpv4([100, 100, 100, 200]);
333
+ const BLOCKED_HOSTS = new Set([
334
+ 'localhost',
335
+ LOOPBACK_V4,
336
+ ANY_V4,
337
+ '::1',
338
+ METADATA_V4_AWS,
339
+ 'metadata.google.internal',
340
+ 'metadata.azure.com',
341
+ METADATA_V4_AZURE,
342
+ 'instance-data',
343
+ ]);
344
+ const host = (env['HOST'] ?? LOOPBACK_V4).trim();
345
+ const port = parsePort(env['PORT']);
346
+ const httpsConfig = buildHttpsConfig();
347
+ const maxConnections = parseInteger(env['SERVER_MAX_CONNECTIONS'], 0, 0);
348
+ const headersTimeoutMs = parseOptionalInteger(env['SERVER_HEADERS_TIMEOUT_MS'], 1);
349
+ const requestTimeoutMs = parseOptionalInteger(env['SERVER_REQUEST_TIMEOUT_MS'], 0);
350
+ const keepAliveTimeoutMs = parseOptionalInteger(env['SERVER_KEEP_ALIVE_TIMEOUT_MS'], 1);
351
+ const keepAliveTimeoutBufferMs = parseOptionalInteger(env['SERVER_KEEP_ALIVE_TIMEOUT_BUFFER_MS'], 0);
352
+ const maxHeadersCount = parseOptionalInteger(env['SERVER_MAX_HEADERS_COUNT'], 1);
353
+ const blockPrivateConnections = parseBoolean(env['SERVER_BLOCK_PRIVATE_CONNECTIONS'], false);
354
+ const allowRemote = parseBoolean(env['ALLOW_REMOTE'], false);
355
+ const requireProtocolVersionHeaderOnSessionInit = parseBoolean(env['MCP_STRICT_PROTOCOL_VERSION_HEADER'], true);
356
+ const baseUrl = new URL(`${httpsConfig.enabled ? 'https' : 'http'}://${formatHostForUrl(host)}:${port}`);
357
+ const runtimeState = {
358
+ httpMode: false,
359
+ };
360
+ export const config = {
361
+ server: {
362
+ name: 'fetch-url-mcp',
363
+ version: serverVersion,
364
+ port,
365
+ host,
366
+ https: httpsConfig,
367
+ sessionTtlMs: DEFAULT_SESSION_TTL_MS,
368
+ sessionInitTimeoutMs: DEFAULT_SESSION_INIT_TIMEOUT_MS,
369
+ maxSessions: DEFAULT_MAX_SESSIONS,
370
+ http: {
371
+ headersTimeoutMs,
372
+ requestTimeoutMs,
373
+ keepAliveTimeoutMs,
374
+ keepAliveTimeoutBufferMs,
375
+ maxHeadersCount,
376
+ maxConnections,
377
+ blockPrivateConnections,
378
+ requireProtocolVersionHeaderOnSessionInit,
379
+ shutdownCloseIdleConnections: true,
380
+ shutdownCloseAllConnections: false,
381
+ },
382
+ },
383
+ fetcher: {
384
+ timeout: DEFAULT_FETCH_TIMEOUT_MS,
385
+ maxRedirects: 5,
386
+ userAgent: env['USER_AGENT'] ?? DEFAULT_USER_AGENT,
387
+ maxContentLength: MAX_HTML_BYTES,
388
+ },
389
+ transform: {
390
+ timeoutMs: DEFAULT_TRANSFORM_TIMEOUT_MS,
391
+ stageWarnRatio: 0.5,
392
+ metadataFormat: 'markdown',
393
+ maxWorkerScale: 4,
394
+ cancelAckTimeoutMs: parseInteger(env['TRANSFORM_CANCEL_ACK_TIMEOUT_MS'], 200, 50, 5000),
395
+ workerMode: parseTransformWorkerMode(env['TRANSFORM_WORKER_MODE']),
396
+ workerResourceLimits: resolveWorkerResourceLimits(),
397
+ },
398
+ tools: {
399
+ enabled: ['fetch-url'],
400
+ timeoutMs: DEFAULT_TOOL_TIMEOUT_MS,
401
+ },
402
+ tasks: {
403
+ maxTotal: DEFAULT_TASKS_MAX_TOTAL,
404
+ maxPerOwner: RESOLVED_TASKS_MAX_PER_OWNER,
405
+ emitStatusNotifications: parseBoolean(env['TASKS_STATUS_NOTIFICATIONS'], false),
406
+ },
407
+ cache: {
408
+ enabled: parseBoolean(env['CACHE_ENABLED'], true),
409
+ ttl: 86400,
410
+ maxKeys: 100,
411
+ maxSizeBytes: 50 * 1024 * 1024, // 50MB
412
+ },
413
+ extraction: {
414
+ maxBlockLength: 5000,
415
+ minParagraphLength: 10,
416
+ },
417
+ noiseRemoval: {
418
+ extraTokens: parseList(env['FETCH_URL_MCP_EXTRA_NOISE_TOKENS']),
419
+ extraSelectors: parseList(env['FETCH_URL_MCP_EXTRA_NOISE_SELECTORS']),
420
+ enabledCategories: [
421
+ 'cookie-banners',
422
+ 'newsletters',
423
+ 'social-share',
424
+ 'nav-footer',
425
+ ],
426
+ debug: false,
427
+ aggressiveMode: false,
428
+ preserveSvgCanvas: false,
429
+ weights: {
430
+ hidden: 50,
431
+ structural: 50,
432
+ promo: 35,
433
+ stickyFixed: 30,
434
+ threshold: 50,
435
+ },
436
+ },
437
+ markdownCleanup: {
438
+ promoteOrphanHeadings: true,
439
+ removeSkipLinks: true,
440
+ removeTocBlocks: true,
441
+ removeTypeDocComments: true,
442
+ headingKeywords: parseListOrDefault(env['MARKDOWN_HEADING_KEYWORDS'], DEFAULT_HEADING_KEYWORDS),
443
+ },
444
+ i18n: {
445
+ locale: normalizeLocale(env['FETCH_URL_MCP_LOCALE']),
446
+ },
447
+ logging: {
448
+ level: parseLogLevel(env['LOG_LEVEL']),
449
+ format: env['LOG_FORMAT']?.toLowerCase() === 'json' ? 'json' : 'text',
450
+ },
451
+ constants: {
452
+ maxHtmlSize: MAX_HTML_BYTES,
453
+ maxUrlLength: 2048,
454
+ maxInlineContentChars: MAX_INLINE_CONTENT_CHARS,
455
+ },
456
+ security: {
457
+ blockedHosts: BLOCKED_HOSTS,
458
+ allowedHosts: parseAllowedHosts(env['ALLOWED_HOSTS']),
459
+ apiKey: env['API_KEY'],
460
+ allowRemote,
461
+ },
462
+ auth: buildAuthConfig(baseUrl),
463
+ rateLimit: {
464
+ enabled: true,
465
+ maxRequests: 100,
466
+ windowMs: 60000,
467
+ cleanupIntervalMs: 60000,
468
+ },
469
+ runtime: runtimeState,
470
+ };
471
+ export function enableHttpMode() {
472
+ runtimeState.httpMode = true;
473
+ }
474
+ const CachedPayloadSchema = z.strictObject({
475
+ content: z.string().optional(),
476
+ markdown: z.string().optional(),
477
+ title: z.string().optional(),
478
+ });
479
+ const CACHE_CONSTANTS = {
480
+ URL_HASH_LENGTH: 32,
481
+ VARY_HASH_LENGTH: 16,
482
+ };
483
+ export function parseCachedPayload(raw) {
484
+ try {
485
+ const parsed = JSON.parse(raw);
486
+ return CachedPayloadSchema.parse(parsed);
487
+ }
488
+ catch {
489
+ return null;
490
+ }
491
+ }
492
+ export function resolveCachedPayloadContent(payload) {
493
+ return payload.markdown ?? payload.content ?? null;
494
+ }
495
+ function createHashFragment(input, length) {
496
+ return sha256Hex(input).substring(0, length);
497
+ }
498
+ function buildCacheKey(namespace, urlHash, varyHash) {
499
+ return varyHash
500
+ ? `${namespace}:${urlHash}.${varyHash}`
501
+ : `${namespace}:${urlHash}`;
502
+ }
503
+ function resolveVaryString(vary) {
504
+ if (typeof vary === 'string')
505
+ return vary;
506
+ try {
507
+ return stableJsonStringify(vary);
508
+ }
509
+ catch {
510
+ return null;
511
+ }
512
+ }
513
+ export function createCacheKey(namespace, url, vary) {
514
+ if (!namespace || !url)
515
+ return null;
516
+ const urlHash = createHashFragment(url, CACHE_CONSTANTS.URL_HASH_LENGTH);
517
+ let varyHash;
518
+ if (vary) {
519
+ const varyString = resolveVaryString(vary);
520
+ if (varyString === null)
521
+ return null;
522
+ if (varyString) {
523
+ varyHash = createHashFragment(varyString, CACHE_CONSTANTS.VARY_HASH_LENGTH);
524
+ }
525
+ }
526
+ return buildCacheKey(namespace, urlHash, varyHash);
527
+ }
528
+ export function parseCacheKey(cacheKey) {
529
+ if (!cacheKey)
530
+ return null;
531
+ const separatorIndex = cacheKey.indexOf(':');
532
+ if (separatorIndex === -1)
533
+ return null;
534
+ const namespace = cacheKey.slice(0, separatorIndex);
535
+ const urlHash = cacheKey.slice(separatorIndex + 1);
536
+ if (!namespace || !urlHash)
537
+ return null;
538
+ return { namespace, urlHash };
539
+ }
540
+ class InMemoryCacheStore {
541
+ max = config.cache.maxKeys;
542
+ maxBytes = config.cache.maxSizeBytes;
543
+ ttlMs = config.cache.ttl * 1000;
544
+ entries = new Map();
545
+ updateEmitter = new EventEmitter();
546
+ currentBytes = 0;
547
+ isEnabled() {
548
+ return config.cache.enabled;
549
+ }
550
+ isExpired(entry, now = Date.now()) {
551
+ return entry.expiresAtMs <= now;
552
+ }
553
+ keys() {
554
+ if (!this.isEnabled())
555
+ return [];
556
+ const now = Date.now();
557
+ const result = [];
558
+ for (const [key, entry] of this.entries) {
559
+ if (!this.isExpired(entry, now))
560
+ result.push(key);
561
+ }
562
+ return result;
563
+ }
564
+ onUpdate(listener) {
565
+ const wrapped = (event) => {
566
+ try {
567
+ const result = listener(event);
568
+ if (result instanceof Promise) {
569
+ void result.catch((error) => {
570
+ this.logError('Cache update listener failed (async)', event.cacheKey, error);
571
+ });
572
+ }
573
+ }
574
+ catch (error) {
575
+ this.logError('Cache update listener failed', event.cacheKey, error);
576
+ }
577
+ };
578
+ this.updateEmitter.on('update', wrapped);
579
+ return () => {
580
+ this.updateEmitter.off('update', wrapped);
581
+ };
582
+ }
583
+ get(cacheKey, options) {
584
+ if (!cacheKey || (!this.isEnabled() && !options?.force))
585
+ return undefined;
586
+ const entry = this.entries.get(cacheKey);
587
+ if (!entry)
588
+ return undefined;
589
+ const now = Date.now();
590
+ if (this.isExpired(entry, now)) {
591
+ this.delete(cacheKey);
592
+ // listChanged=false: lazy eviction on read is silent — only writes change
593
+ // the list. Clients must not rely on list-changed events from reads.
594
+ this.notify(cacheKey, false);
595
+ return undefined;
596
+ }
597
+ // Refresh LRU position
598
+ this.entries.delete(cacheKey);
599
+ this.entries.set(cacheKey, entry);
600
+ return entry;
601
+ }
602
+ delete(cacheKey) {
603
+ const entry = this.entries.get(cacheKey);
604
+ if (entry) {
605
+ this.currentBytes -= entry.content.length;
606
+ this.entries.delete(cacheKey);
607
+ return true;
608
+ }
609
+ return false;
610
+ }
611
+ evictOldestEntry() {
612
+ const firstKey = this.entries.keys().next();
613
+ return !firstKey.done && this.delete(firstKey.value);
614
+ }
615
+ set(cacheKey, content, metadata, options) {
616
+ if (!cacheKey || !content)
617
+ return;
618
+ if (!this.isEnabled() && !options?.force)
619
+ return;
620
+ const now = Date.now();
621
+ const expiresAtMs = now + this.ttlMs;
622
+ // Check size limit before insertion
623
+ const entrySize = content.length;
624
+ if (entrySize > this.maxBytes) {
625
+ logWarn('Cache entry exceeds max size', {
626
+ key: cacheKey,
627
+ size: entrySize,
628
+ max: this.maxBytes,
629
+ });
630
+ return;
631
+ }
632
+ let listChanged = !this.entries.has(cacheKey);
633
+ // Evict if needed (size-based)
634
+ while (this.currentBytes + entrySize > this.maxBytes) {
635
+ if (this.evictOldestEntry()) {
636
+ listChanged = true;
637
+ }
638
+ else {
639
+ break;
640
+ }
641
+ }
642
+ const entry = {
643
+ url: metadata.url,
644
+ content,
645
+ fetchedAt: new Date(now).toISOString(),
646
+ expiresAt: new Date(expiresAtMs).toISOString(),
647
+ expiresAtMs,
648
+ ...(metadata.title ? { title: metadata.title } : {}),
649
+ };
650
+ if (this.entries.has(cacheKey)) {
651
+ this.delete(cacheKey);
652
+ }
653
+ this.entries.set(cacheKey, entry);
654
+ this.currentBytes += entrySize;
655
+ // Eviction (LRU: first insertion-order key) - Count based
656
+ if (this.entries.size > this.max && this.evictOldestEntry()) {
657
+ listChanged = true;
658
+ }
659
+ this.notify(cacheKey, listChanged);
660
+ }
661
+ notify(cacheKey, listChanged) {
662
+ if (this.updateEmitter.listenerCount('update') === 0)
663
+ return;
664
+ const parts = parseCacheKey(cacheKey);
665
+ if (!parts)
666
+ return;
667
+ this.updateEmitter.emit('update', { cacheKey, ...parts, listChanged });
668
+ }
669
+ /**
670
+ * Read an entry without updating its LRU position.
671
+ * Use this for metadata access (e.g. resource listing) to avoid polluting the
672
+ * eviction order; expired entries are treated as absent but not evicted here.
673
+ */
674
+ peek(cacheKey) {
675
+ if (!cacheKey)
676
+ return undefined;
677
+ const entry = this.entries.get(cacheKey);
678
+ if (!entry)
679
+ return undefined;
680
+ if (this.isExpired(entry))
681
+ return undefined;
682
+ return entry;
683
+ }
684
+ logError(message, cacheKey, error) {
685
+ logWarn(message, {
686
+ key: cacheKey.length > 100 ? cacheKey.slice(0, 100) : cacheKey,
687
+ error: getErrorMessage(error),
688
+ });
689
+ }
690
+ }
691
+ const store = new InMemoryCacheStore();
692
+ export function onCacheUpdate(listener) {
693
+ return store.onUpdate(listener);
694
+ }
695
+ export function get(cacheKey, options) {
696
+ return store.get(cacheKey, options);
697
+ }
698
+ export function set(cacheKey, content, metadata, options) {
699
+ store.set(cacheKey, content, metadata, options);
700
+ }
701
+ export function keys() {
702
+ return store.keys();
703
+ }
704
+ export function getEntryMeta(cacheKey) {
705
+ const entry = store.peek(cacheKey);
706
+ if (!entry)
707
+ return undefined;
708
+ return entry.title !== undefined
709
+ ? { url: entry.url, title: entry.title }
710
+ : { url: entry.url };
711
+ }
712
+ export function isEnabled() {
713
+ return store.isEnabled();
714
+ }
715
+ function hasPackageJsonVersion(value) {
716
+ if (typeof value !== 'object' || value === null)
717
+ return false;
718
+ const record = value;
719
+ return typeof record.version === 'string';
720
+ }
721
+ function readServerVersion(moduleUrl) {
722
+ const packageJsonPath = findPackageJSON(moduleUrl);
723
+ if (!packageJsonPath)
724
+ throw new Error('package.json not found');
725
+ let packageJson;
726
+ try {
727
+ packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
728
+ }
729
+ catch (error) {
730
+ throw new Error(`Failed to parse package.json at ${packageJsonPath}: ${getErrorMessage(error)}`, { cause: error });
731
+ }
732
+ if (!hasPackageJsonVersion(packageJson)) {
733
+ throw new Error(`package.json version is missing at ${packageJsonPath}`);
734
+ }
735
+ return packageJson.version;
736
+ }
737
+ const requestContext = new AsyncLocalStorage({
738
+ name: 'requestContext',
739
+ });
740
+ let mcpServer;
741
+ const sessionServers = new Map();
742
+ let stderrAvailable = true;
743
+ process.stderr.on('error', () => {
744
+ stderrAvailable = false;
745
+ });
746
+ export function setMcpServer(server) {
747
+ mcpServer = server;
748
+ }
749
+ export function registerMcpSessionServer(sessionId, server) {
750
+ if (!sessionId)
751
+ return;
752
+ sessionServers.set(sessionId, server);
753
+ }
754
+ export function unregisterMcpSessionServer(sessionId) {
755
+ if (!sessionId)
756
+ return;
757
+ sessionServers.delete(sessionId);
758
+ }
759
+ export function unregisterMcpSessionServerByServer(server) {
760
+ for (const [sessionId, mappedServer] of sessionServers.entries()) {
761
+ if (mappedServer !== server)
762
+ continue;
763
+ sessionServers.delete(sessionId);
764
+ }
765
+ }
766
+ export function resolveMcpSessionIdByServer(server) {
767
+ for (const [sessionId, mappedServer] of sessionServers.entries()) {
768
+ if (mappedServer === server)
769
+ return sessionId;
770
+ }
771
+ return undefined;
772
+ }
773
+ export function runWithRequestContext(context, fn) {
774
+ return requestContext.run(context, fn);
775
+ }
776
+ function getRequestContext() {
777
+ return requestContext.getStore();
778
+ }
779
+ export function getRequestId() {
780
+ const context = getRequestContext();
781
+ return context?.requestId;
782
+ }
783
+ function getSessionId() {
784
+ return getRequestContext()?.sessionId;
785
+ }
786
+ export function getOperationId() {
787
+ return getRequestContext()?.operationId;
788
+ }
789
+ function isDebugEnabled() {
790
+ return config.logging.level === 'debug';
791
+ }
792
+ function buildContextMetadata() {
793
+ const ctx = requestContext.getStore();
794
+ if (!ctx)
795
+ return undefined;
796
+ const { requestId, operationId, sessionId } = ctx;
797
+ const includeSession = sessionId && isDebugEnabled();
798
+ if (!requestId && !operationId && !includeSession)
799
+ return undefined;
800
+ const meta = {};
801
+ if (requestId)
802
+ meta['requestId'] = requestId;
803
+ if (operationId)
804
+ meta['operationId'] = operationId;
805
+ if (includeSession)
806
+ meta['sessionId'] = sessionId;
807
+ return meta;
808
+ }
809
+ function mergeMetadata(meta) {
810
+ const contextMeta = buildContextMetadata();
811
+ const hasMeta = meta && Object.keys(meta).length > 0;
812
+ if (!contextMeta && !hasMeta)
813
+ return undefined;
814
+ if (!contextMeta)
815
+ return meta;
816
+ if (!hasMeta)
817
+ return contextMeta;
818
+ return { ...contextMeta, ...meta };
819
+ }
820
+ function formatMetadata(meta) {
821
+ const merged = mergeMetadata(meta);
822
+ if (!merged)
823
+ return '';
824
+ return ` ${inspect(merged, { breakLength: Infinity, colors: false, compact: true, sorted: true })}`;
825
+ }
826
+ function createTimestamp() {
827
+ return new Date().toISOString();
828
+ }
829
+ function formatLogEntry(level, message, meta) {
830
+ if (config.logging.format === 'json') {
831
+ const merged = mergeMetadata(meta);
832
+ const entry = {
833
+ timestamp: createTimestamp(),
834
+ level: level.toUpperCase(),
835
+ message,
836
+ };
837
+ if (merged) {
838
+ Object.assign(entry, merged);
839
+ }
840
+ return JSON.stringify(entry);
841
+ }
842
+ return `[${createTimestamp()}] ${level.toUpperCase()}: ${message}${formatMetadata(meta)}`;
843
+ }
844
+ const LEVEL_PRIORITY = {
845
+ debug: 0,
846
+ info: 1,
847
+ warn: 2,
848
+ error: 3,
849
+ };
850
+ function shouldLog(level) {
851
+ return LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[config.logging.level];
852
+ }
853
+ function mapToMcpLevel(level) {
854
+ switch (level) {
855
+ case 'warn':
856
+ return 'warning';
857
+ case 'error':
858
+ return 'error';
859
+ case 'debug':
860
+ return 'debug';
861
+ case 'info':
862
+ default:
863
+ return 'info';
864
+ }
865
+ }
866
+ function resolveErrorText(err) {
867
+ if (err instanceof Error)
868
+ return err.message;
869
+ if (typeof err === 'string')
870
+ return err;
871
+ return 'unknown error';
872
+ }
873
+ function safeWriteStderr(line) {
874
+ if (!stderrAvailable)
875
+ return;
876
+ if (process.stderr.destroyed || process.stderr.writableEnded) {
877
+ stderrAvailable = false;
878
+ return;
879
+ }
880
+ try {
881
+ process.stderr.write(line);
882
+ }
883
+ catch {
884
+ // Logging must never take down the process (e.g. EPIPE).
885
+ stderrAvailable = false;
886
+ }
887
+ }
888
+ function writeLog(level, message, meta) {
889
+ if (!shouldLog(level))
890
+ return;
891
+ const line = formatLogEntry(level, message, meta);
892
+ safeWriteStderr(`${stripVTControlCharacters(line)}\n`);
893
+ const sessionId = getSessionId();
894
+ const server = sessionId
895
+ ? (sessionServers.get(sessionId) ?? mcpServer)
896
+ : mcpServer;
897
+ if (!server)
898
+ return;
899
+ if (!server.isConnected())
900
+ return;
901
+ try {
902
+ server.server
903
+ .sendLoggingMessage({
904
+ level: mapToMcpLevel(level),
905
+ logger: 'fetch-url-mcp',
906
+ // Preserve existing behavior: MCP payload includes only message + provided meta (not ALS context meta).
907
+ data: meta ? { message, ...meta } : message,
908
+ }, sessionId)
909
+ .catch((err) => {
910
+ if (!isDebugEnabled())
911
+ return;
912
+ const errorText = resolveErrorText(err);
913
+ safeWriteStderr(`[${createTimestamp()}] WARN: Failed to forward log to MCP${sessionId ? ` (sessionId=${sessionId})` : ''}: ${errorText}\n`);
914
+ });
915
+ }
916
+ catch (err) {
917
+ if (!isDebugEnabled())
918
+ return;
919
+ const errorText = resolveErrorText(err);
920
+ safeWriteStderr(`[${createTimestamp()}] WARN: Failed to forward log to MCP (sync error): ${errorText}\n`);
921
+ }
922
+ }
923
+ export function logInfo(message, meta) {
924
+ writeLog('info', message, meta);
925
+ }
926
+ export function logDebug(message, meta) {
927
+ writeLog('debug', message, meta);
928
+ }
929
+ export function logWarn(message, meta) {
930
+ writeLog('warn', message, meta);
931
+ }
932
+ export function logError(message, error) {
933
+ const errorMeta = error instanceof Error
934
+ ? { error: error.message, stack: error.stack }
935
+ : (error ?? {});
936
+ writeLog('error', message, errorMeta);
937
+ }
938
+ export function setLogLevel(level) {
939
+ const normalized = level.toLowerCase();
940
+ // Map MCP logging levels (RFC 5424 subset) to internal levels.
941
+ if (normalized === 'debug') {
942
+ config.logging.level = 'debug';
943
+ }
944
+ else if (normalized === 'info' || normalized === 'notice') {
945
+ config.logging.level = 'info';
946
+ }
947
+ else if (normalized === 'warning' || normalized === 'warn') {
948
+ config.logging.level = 'warn';
949
+ }
950
+ else if (normalized === 'error' ||
951
+ normalized === 'critical' ||
952
+ normalized === 'alert' ||
953
+ normalized === 'emergency') {
954
+ config.logging.level = 'error';
955
+ }
956
+ }
957
+ export function redactUrl(rawUrl) {
958
+ try {
959
+ const url = new URL(rawUrl);
960
+ url.username = '';
961
+ url.password = '';
962
+ url.hash = '';
963
+ url.search = '';
964
+ return url.toString();
965
+ }
966
+ catch {
967
+ return rawUrl;
968
+ }
969
+ }
970
+ export function composeCloseHandlers(first, second) {
971
+ if (!first)
972
+ return second;
973
+ if (!second)
974
+ return first;
975
+ return () => {
976
+ try {
977
+ first();
978
+ }
979
+ finally {
980
+ second();
981
+ }
982
+ };
983
+ }
984
+ const MIN_CLEANUP_INTERVAL_MS = 10_000;
985
+ const MAX_CLEANUP_INTERVAL_MS = 60_000;
986
+ const SESSION_CLOSE_BATCH_SIZE = 10;
987
+ function getCleanupIntervalMs(sessionTtlMs) {
988
+ return Math.min(Math.max(Math.floor(sessionTtlMs / 2), MIN_CLEANUP_INTERVAL_MS), MAX_CLEANUP_INTERVAL_MS);
989
+ }
990
+ function handleSessionCleanupError(error) {
991
+ if (isAbortError(error))
992
+ return;
993
+ logWarn('Session cleanup loop failed', { error: getErrorMessage(error) });
994
+ }
995
+ function getRejectedSettledResult(result) {
996
+ return result.status === 'rejected' ? result : undefined;
997
+ }
998
+ function logRejectedSettledResults(results, message) {
999
+ for (const result of results) {
1000
+ const rejected = getRejectedSettledResult(result);
1001
+ if (!rejected)
1002
+ continue;
1003
+ logWarn(message, { error: getErrorMessage(rejected.reason) });
1004
+ }
1005
+ }
1006
+ function isSessionExpired(session, now, sessionTtlMs) {
1007
+ if (sessionTtlMs <= 0)
1008
+ return false;
1009
+ return now - session.lastSeen > sessionTtlMs;
1010
+ }
1011
+ function chunkArray(items, size) {
1012
+ const chunks = [];
1013
+ for (let index = 0; index < items.length; index += size) {
1014
+ chunks.push(items.slice(index, index + size));
1015
+ }
1016
+ return chunks;
1017
+ }
1018
+ class SessionCleanupLoop {
1019
+ store;
1020
+ sessionTtlMs;
1021
+ onEvictSession;
1022
+ cleanupIntervalMsOverride;
1023
+ constructor(store, sessionTtlMs, onEvictSession, cleanupIntervalMsOverride) {
1024
+ this.store = store;
1025
+ this.sessionTtlMs = sessionTtlMs;
1026
+ this.onEvictSession = onEvictSession;
1027
+ this.cleanupIntervalMsOverride = cleanupIntervalMsOverride;
1028
+ }
1029
+ start() {
1030
+ const controller = new AbortController();
1031
+ const intervalMs = this.cleanupIntervalMsOverride ?? getCleanupIntervalMs(this.sessionTtlMs);
1032
+ startAbortableIntervalLoop(intervalMs, Date.now, {
1033
+ signal: controller.signal,
1034
+ onTick: async (getNow) => {
1035
+ await this.handleTick(getNow(), controller.signal);
1036
+ },
1037
+ onError: handleSessionCleanupError,
1038
+ });
1039
+ return controller;
1040
+ }
1041
+ async handleTick(now, signal) {
1042
+ const evicted = this.store.evictExpired();
1043
+ for (const batch of chunkArray(evicted, SESSION_CLOSE_BATCH_SIZE)) {
1044
+ const results = await Promise.allSettled(batch.map(async (session) => this.closeExpiredSession(session)));
1045
+ logRejectedSettledResults(results, 'Failed to process expired session cleanup task');
1046
+ if (signal.aborted)
1047
+ return;
1048
+ }
1049
+ if (evicted.length > 0) {
1050
+ logInfo('Expired sessions evicted', {
1051
+ evicted: evicted.length,
1052
+ timestamp: new Date(now).toISOString(),
1053
+ });
1054
+ }
1055
+ }
1056
+ async closeExpiredSession(session) {
1057
+ if (this.onEvictSession) {
1058
+ try {
1059
+ await this.onEvictSession(session);
1060
+ }
1061
+ catch (error) {
1062
+ logWarn('Expired session pre-close hook failed', {
1063
+ error: getErrorMessage(error),
1064
+ });
1065
+ }
1066
+ }
1067
+ try {
1068
+ unregisterMcpSessionServerByServer(session.server);
1069
+ }
1070
+ catch (error) {
1071
+ logWarn('Failed to unregister session server', {
1072
+ error: getErrorMessage(error),
1073
+ });
1074
+ }
1075
+ const [transportResult, serverResult] = await Promise.allSettled([
1076
+ session.transport.close(),
1077
+ session.server.close(),
1078
+ ]);
1079
+ const transportRejected = getRejectedSettledResult(transportResult);
1080
+ const serverRejected = getRejectedSettledResult(serverResult);
1081
+ this.logCloseFailure('transport', transportRejected?.reason);
1082
+ this.logCloseFailure('server', serverRejected?.reason);
1083
+ }
1084
+ logCloseFailure(target, error) {
1085
+ if (error == null)
1086
+ return;
1087
+ logWarn(`Failed to close expired session ${target}`, {
1088
+ error: getErrorMessage(error),
1089
+ });
1090
+ }
1091
+ }
1092
+ export function startSessionCleanupLoop(store, sessionTtlMs, options) {
1093
+ return new SessionCleanupLoop(store, sessionTtlMs, options?.onEvictSession, options?.cleanupIntervalMs).start();
1094
+ }
1095
+ function moveSessionToEnd(sessions, sessionId, session) {
1096
+ sessions.delete(sessionId);
1097
+ sessions.set(sessionId, session);
1098
+ }
1099
+ function removeSessionById(sessions, sessionId) {
1100
+ const session = sessions.get(sessionId);
1101
+ sessions.delete(sessionId);
1102
+ return session;
1103
+ }
1104
+ function isBlankSessionId(sessionId) {
1105
+ return sessionId.length === 0;
1106
+ }
1107
+ class InMemorySessionStore {
1108
+ sessionTtlMs;
1109
+ sessions = new Map();
1110
+ inflight = 0;
1111
+ constructor(sessionTtlMs) {
1112
+ this.sessionTtlMs = sessionTtlMs;
1113
+ }
1114
+ get(sessionId) {
1115
+ if (isBlankSessionId(sessionId))
1116
+ return undefined;
1117
+ return this.sessions.get(sessionId);
1118
+ }
1119
+ touch(sessionId) {
1120
+ if (isBlankSessionId(sessionId))
1121
+ return;
1122
+ const session = this.sessions.get(sessionId);
1123
+ if (!session)
1124
+ return;
1125
+ session.lastSeen = Date.now();
1126
+ moveSessionToEnd(this.sessions, sessionId, session);
1127
+ }
1128
+ set(sessionId, entry) {
1129
+ if (isBlankSessionId(sessionId))
1130
+ return;
1131
+ moveSessionToEnd(this.sessions, sessionId, entry);
1132
+ }
1133
+ remove(sessionId) {
1134
+ if (isBlankSessionId(sessionId))
1135
+ return undefined;
1136
+ return removeSessionById(this.sessions, sessionId);
1137
+ }
1138
+ size() {
1139
+ return this.sessions.size;
1140
+ }
1141
+ inFlight() {
1142
+ return this.inflight;
1143
+ }
1144
+ incrementInFlight() {
1145
+ this.inflight += 1;
1146
+ }
1147
+ decrementInFlight() {
1148
+ if (this.inflight === 0)
1149
+ return;
1150
+ this.inflight -= 1;
1151
+ }
1152
+ clear() {
1153
+ const entries = [...this.sessions.values()];
1154
+ this.sessions.clear();
1155
+ return entries;
1156
+ }
1157
+ evictExpired() {
1158
+ const now = Date.now();
1159
+ const evicted = [];
1160
+ for (const [id, session] of this.sessions.entries()) {
1161
+ if (!isSessionExpired(session, now, this.sessionTtlMs))
1162
+ continue;
1163
+ this.sessions.delete(id);
1164
+ evicted.push(session);
1165
+ }
1166
+ return evicted;
1167
+ }
1168
+ evictOldest() {
1169
+ const oldest = this.sessions.keys().next();
1170
+ if (oldest.done)
1171
+ return undefined;
1172
+ return removeSessionById(this.sessions, oldest.value);
1173
+ }
1174
+ }
1175
+ export function createSessionStore(sessionTtlMs) {
1176
+ return new InMemorySessionStore(sessionTtlMs);
1177
+ }
1178
+ class SessionSlotTracker {
1179
+ store;
1180
+ slotReleased = false;
1181
+ initialized = false;
1182
+ constructor(store) {
1183
+ this.store = store;
1184
+ }
1185
+ releaseSlot() {
1186
+ if (this.slotReleased)
1187
+ return;
1188
+ this.slotReleased = true;
1189
+ this.store.decrementInFlight();
1190
+ }
1191
+ markInitialized() {
1192
+ this.initialized = true;
1193
+ }
1194
+ isInitialized() {
1195
+ return this.initialized;
1196
+ }
1197
+ }
1198
+ export function createSlotTracker(store) {
1199
+ return new SessionSlotTracker(store);
1200
+ }
1201
+ function currentLoad(store) {
1202
+ return store.size() + store.inFlight();
1203
+ }
1204
+ export function reserveSessionSlot(store, maxSessions) {
1205
+ if (maxSessions <= 0)
1206
+ return false;
1207
+ if (currentLoad(store) >= maxSessions)
1208
+ return false;
1209
+ store.incrementInFlight();
1210
+ return true;
1211
+ }
1212
+ function isAtCapacity(store, maxSessions) {
1213
+ return currentLoad(store) >= maxSessions;
1214
+ }
1215
+ export function ensureSessionCapacity({ store, maxSessions, evictOldest, }) {
1216
+ if (maxSessions <= 0)
1217
+ return false;
1218
+ const currentSize = store.size();
1219
+ const inflight = store.inFlight();
1220
+ if (currentSize + inflight < maxSessions)
1221
+ return true;
1222
+ const canFreeSlot = currentSize >= maxSessions && currentSize - 1 + inflight < maxSessions;
1223
+ if (!canFreeSlot)
1224
+ return false;
1225
+ if (!evictOldest(store))
1226
+ return false;
1227
+ return !isAtCapacity(store, maxSessions);
1228
+ }