@j0hanz/fetch-url-mcp 1.1.1 → 1.1.2

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/dist/cache.js CHANGED
@@ -93,13 +93,16 @@ class InMemoryCacheStore {
93
93
  isEnabled() {
94
94
  return config.cache.enabled;
95
95
  }
96
+ isExpired(entry, now = Date.now()) {
97
+ return entry.expiresAtMs <= now;
98
+ }
96
99
  keys() {
97
100
  if (!this.isEnabled())
98
101
  return [];
99
102
  const now = Date.now();
100
103
  const result = [];
101
104
  for (const [key, entry] of this.entries) {
102
- if (entry.expiresAtMs > now)
105
+ if (!this.isExpired(entry, now))
103
106
  result.push(key);
104
107
  }
105
108
  return result;
@@ -130,7 +133,7 @@ class InMemoryCacheStore {
130
133
  if (!entry)
131
134
  return undefined;
132
135
  const now = Date.now();
133
- if (entry.expiresAtMs <= now) {
136
+ if (this.isExpired(entry, now)) {
134
137
  this.delete(cacheKey);
135
138
  this.notify(cacheKey, true);
136
139
  return undefined;
package/dist/cli.js CHANGED
@@ -19,6 +19,17 @@ const optionSchema = {
19
19
  function toErrorMessage(error) {
20
20
  return error instanceof Error ? error.message : String(error);
21
21
  }
22
+ function toBoolean(value) {
23
+ return value === true;
24
+ }
25
+ function buildCliValues(values) {
26
+ const { stdio, help, version } = values;
27
+ return {
28
+ stdio: toBoolean(stdio),
29
+ help: toBoolean(help),
30
+ version: toBoolean(version),
31
+ };
32
+ }
22
33
  export function renderCliUsage() {
23
34
  return `${usageLines.join('\n')}\n`;
24
35
  }
@@ -32,11 +43,7 @@ export function parseCliArgs(args) {
32
43
  });
33
44
  return {
34
45
  ok: true,
35
- values: {
36
- stdio: values.stdio,
37
- help: values.help,
38
- version: values.version,
39
- },
46
+ values: buildCliValues(values),
40
47
  };
41
48
  }
42
49
  catch (error) {
package/dist/config.js CHANGED
@@ -233,24 +233,25 @@ const RESOLVED_TASKS_MAX_PER_OWNER = Math.min(DEFAULT_TASKS_MAX_PER_OWNER, DEFAU
233
233
  function resolveWorkerResourceLimits() {
234
234
  const limits = {};
235
235
  let hasAny = false;
236
- const maxOldGenerationSizeMb = parseOptionalInteger(env['TRANSFORM_WORKER_MAX_OLD_GENERATION_MB'], 1);
237
- const maxYoungGenerationSizeMb = parseOptionalInteger(env['TRANSFORM_WORKER_MAX_YOUNG_GENERATION_MB'], 1);
238
- const codeRangeSizeMb = parseOptionalInteger(env['TRANSFORM_WORKER_CODE_RANGE_MB'], 1);
239
- const stackSizeMb = parseOptionalInteger(env['TRANSFORM_WORKER_STACK_MB'], 1);
240
- if (maxOldGenerationSizeMb !== undefined) {
241
- limits.maxOldGenerationSizeMb = maxOldGenerationSizeMb;
242
- hasAny = true;
243
- }
244
- if (maxYoungGenerationSizeMb !== undefined) {
245
- limits.maxYoungGenerationSizeMb = maxYoungGenerationSizeMb;
246
- hasAny = true;
247
- }
248
- if (codeRangeSizeMb !== undefined) {
249
- limits.codeRangeSizeMb = codeRangeSizeMb;
250
- hasAny = true;
251
- }
252
- if (stackSizeMb !== undefined) {
253
- limits.stackSizeMb = stackSizeMb;
236
+ const entries = [
237
+ [
238
+ 'maxOldGenerationSizeMb',
239
+ parseOptionalInteger(env['TRANSFORM_WORKER_MAX_OLD_GENERATION_MB'], 1),
240
+ ],
241
+ [
242
+ 'maxYoungGenerationSizeMb',
243
+ parseOptionalInteger(env['TRANSFORM_WORKER_MAX_YOUNG_GENERATION_MB'], 1),
244
+ ],
245
+ [
246
+ 'codeRangeSizeMb',
247
+ parseOptionalInteger(env['TRANSFORM_WORKER_CODE_RANGE_MB'], 1),
248
+ ],
249
+ ['stackSizeMb', parseOptionalInteger(env['TRANSFORM_WORKER_STACK_MB'], 1)],
250
+ ];
251
+ for (const [key, value] of entries) {
252
+ if (value === undefined)
253
+ continue;
254
+ limits[key] = value;
254
255
  hasAny = true;
255
256
  }
256
257
  return hasAny ? limits : undefined;
package/dist/crypto.js CHANGED
@@ -18,6 +18,11 @@ function assertAllowedAlgorithm(algorithm) {
18
18
  throw new Error(`Hash algorithm not allowed: ${algorithm}`);
19
19
  }
20
20
  }
21
+ function padBuffer(buffer, length) {
22
+ const padded = Buffer.alloc(length);
23
+ buffer.copy(padded);
24
+ return padded;
25
+ }
21
26
  export function timingSafeEqualUtf8(a, b) {
22
27
  const aBuffer = Buffer.from(a, 'utf8');
23
28
  const bBuffer = Buffer.from(b, 'utf8');
@@ -26,10 +31,8 @@ export function timingSafeEqualUtf8(a, b) {
26
31
  }
27
32
  // Avoid early return timing differences on length mismatch.
28
33
  const maxLength = Math.max(aBuffer.length, bBuffer.length);
29
- const paddedA = Buffer.alloc(maxLength);
30
- const paddedB = Buffer.alloc(maxLength);
31
- aBuffer.copy(paddedA);
32
- bBuffer.copy(paddedB);
34
+ const paddedA = padBuffer(aBuffer, maxLength);
35
+ const paddedB = padBuffer(bBuffer, maxLength);
33
36
  return timingSafeEqual(paddedA, paddedB) && aBuffer.length === bBuffer.length;
34
37
  }
35
38
  function hashHex(algorithm, input) {
@@ -103,22 +103,25 @@ function buildTokenRegex(tokens) {
103
103
  return NO_MATCH_REGEX;
104
104
  return new RegExp(`(?:^|[^a-z0-9])(?:${[...tokens].map(escapeRegexLiteral).join('|')})(?:$|[^a-z0-9])`, 'i');
105
105
  }
106
+ function addTokens(target, tokens) {
107
+ for (const token of tokens)
108
+ target.add(token);
109
+ }
106
110
  function getPromoMatchers(currentConfig, flags) {
107
111
  const baseTokens = new Set(PROMO_TOKENS_ALWAYS);
108
112
  const aggressiveTokens = new Set();
109
113
  if (currentConfig.aggressiveMode) {
110
- for (const t of PROMO_TOKENS_AGGRESSIVE)
111
- aggressiveTokens.add(t);
114
+ addTokens(aggressiveTokens, PROMO_TOKENS_AGGRESSIVE);
115
+ }
116
+ if (flags.cookieBanners) {
117
+ addTokens(baseTokens, PROMO_TOKENS_BY_CATEGORY['cookie-banners']);
118
+ }
119
+ if (flags.newsletters) {
120
+ addTokens(baseTokens, PROMO_TOKENS_BY_CATEGORY['newsletters']);
121
+ }
122
+ if (flags.socialShare) {
123
+ addTokens(baseTokens, PROMO_TOKENS_BY_CATEGORY['social-share']);
112
124
  }
113
- if (flags.cookieBanners)
114
- for (const t of PROMO_TOKENS_BY_CATEGORY['cookie-banners'])
115
- baseTokens.add(t);
116
- if (flags.newsletters)
117
- for (const t of PROMO_TOKENS_BY_CATEGORY['newsletters'])
118
- baseTokens.add(t);
119
- if (flags.socialShare)
120
- for (const t of PROMO_TOKENS_BY_CATEGORY['social-share'])
121
- baseTokens.add(t);
122
125
  for (const t of currentConfig.extraTokens) {
123
126
  const n = t.toLowerCase().trim();
124
127
  if (n)
package/dist/errors.js CHANGED
@@ -19,17 +19,20 @@ export class FetchError extends Error {
19
19
  export function getErrorMessage(error) {
20
20
  if (isError(error))
21
21
  return error.message;
22
- if (typeof error === 'string' && error.length > 0)
22
+ if (isNonEmptyString(error))
23
23
  return error;
24
24
  if (isErrorWithMessage(error))
25
25
  return error.message;
26
26
  return formatUnknownError(error);
27
27
  }
28
+ function isNonEmptyString(value) {
29
+ return typeof value === 'string' && value.length > 0;
30
+ }
28
31
  function isErrorWithMessage(error) {
29
32
  if (!isObject(error))
30
33
  return false;
31
34
  const { message } = error;
32
- return typeof message === 'string' && message.length > 0;
35
+ return isNonEmptyString(message);
33
36
  }
34
37
  function formatUnknownError(error) {
35
38
  if (error === null || error === undefined)
package/dist/fetch.js CHANGED
@@ -569,8 +569,11 @@ function createTooManyRedirectsFetchError(url) {
569
569
  function createMissingRedirectLocationFetchError(url) {
570
570
  return new FetchError('Redirect response missing Location header', url);
571
571
  }
572
+ function buildNetworkErrorMessage(url) {
573
+ return `Network error: Could not reach ${url}`;
574
+ }
572
575
  function createNetworkFetchError(url, message) {
573
- return new FetchError(`Network error: Could not reach ${url}`, url, undefined, message ? { message } : {});
576
+ return new FetchError(buildNetworkErrorMessage(url), url, undefined, message ? { message } : {});
574
577
  }
575
578
  function createUnknownFetchError(url, message) {
576
579
  return new FetchError(message, url);
@@ -619,7 +622,7 @@ function mapFetchError(error, fallbackUrl, timeoutMs) {
619
622
  code === 'EINVAL') {
620
623
  return new FetchError(error.message, url, 400, { code });
621
624
  }
622
- return new FetchError(`Network error: Could not reach ${url}`, url, undefined, {
625
+ return new FetchError(buildNetworkErrorMessage(url), url, undefined, {
623
626
  code,
624
627
  message: error.message,
625
628
  });
@@ -1,7 +1,7 @@
1
1
  import { isIP, SocketAddress } from 'node:net';
2
2
  import { domainToASCII } from 'node:url';
3
3
  export function normalizeHost(value) {
4
- const trimmedLower = value.trim().toLowerCase();
4
+ const trimmedLower = trimToNull(value)?.toLowerCase();
5
5
  if (!trimmedLower)
6
6
  return null;
7
7
  const first = takeFirstHostValue(trimmedLower);
@@ -25,10 +25,7 @@ function takeFirstHostValue(value) {
25
25
  // Faster than split(',') for large forwarded headers; preserves behavior.
26
26
  const commaIndex = value.indexOf(',');
27
27
  const first = commaIndex === -1 ? value : value.slice(0, commaIndex);
28
- if (!first)
29
- return null;
30
- const trimmed = first.trim();
31
- return trimmed ? trimmed : null;
28
+ return first ? trimToNull(first) : null;
32
29
  }
33
30
  function stripIpv6Brackets(value) {
34
31
  if (!value.startsWith('['))
@@ -48,7 +45,7 @@ function isIpV6Literal(value) {
48
45
  return isIP(value) === 6;
49
46
  }
50
47
  function normalizeHostname(value) {
51
- const trimmed = value.trim().toLowerCase();
48
+ const trimmed = trimToNull(value)?.toLowerCase();
52
49
  if (!trimmed)
53
50
  return null;
54
51
  if (isIP(trimmed))
@@ -68,6 +65,10 @@ function parseHostWithUrl(value) {
68
65
  return null;
69
66
  }
70
67
  }
68
+ function trimToNull(value) {
69
+ const trimmed = value.trim();
70
+ return trimmed ? trimmed : null;
71
+ }
71
72
  function stripTrailingDots(value) {
72
73
  // Keep loop (rather than regex) to preserve exact behavior and avoid hidden allocations.
73
74
  let result = value;
@@ -63,15 +63,13 @@ function createTransportAdapter(transportImpl) {
63
63
  function sendJson(res, status, body) {
64
64
  res.statusCode = status;
65
65
  res.setHeader('Content-Type', 'application/json; charset=utf-8');
66
- res.setHeader('X-Content-Type-Options', 'nosniff');
67
- res.setHeader('Cache-Control', 'no-store');
66
+ setNoStoreHeaders(res);
68
67
  res.end(JSON.stringify(body));
69
68
  }
70
69
  function sendText(res, status, body) {
71
70
  res.statusCode = status;
72
71
  res.setHeader('Content-Type', 'text/plain; charset=utf-8');
73
- res.setHeader('X-Content-Type-Options', 'nosniff');
74
- res.setHeader('Cache-Control', 'no-store');
72
+ setNoStoreHeaders(res);
75
73
  res.end(body);
76
74
  }
77
75
  function sendEmpty(res, status) {
@@ -79,6 +77,10 @@ function sendEmpty(res, status) {
79
77
  res.setHeader('Content-Length', '0');
80
78
  res.end();
81
79
  }
80
+ function setNoStoreHeaders(res) {
81
+ res.setHeader('X-Content-Type-Options', 'nosniff');
82
+ res.setHeader('Cache-Control', 'no-store');
83
+ }
82
84
  function drainRequest(req) {
83
85
  if (req.readableEnded)
84
86
  return;
package/dist/index.js CHANGED
@@ -7,6 +7,9 @@ import { logError } from './observability.js';
7
7
  import { startStdioServer } from './server.js';
8
8
  const FORCE_EXIT_TIMEOUT_MS = 10_000;
9
9
  let forcedExitTimer;
10
+ function toError(value) {
11
+ return value instanceof Error ? value : new Error(String(value));
12
+ }
10
13
  function scheduleForcedExit(reason) {
11
14
  if (forcedExitTimer)
12
15
  return;
@@ -69,8 +72,7 @@ process.on('uncaughtException', (error) => {
69
72
  handleFatalError('Uncaught exception', error, 'UNCAUGHT_EXCEPTION');
70
73
  });
71
74
  process.on('unhandledRejection', (reason) => {
72
- const error = reason instanceof Error ? reason : new Error(String(reason));
73
- handleFatalError('Unhandled rejection', error, 'UNHANDLED_REJECTION');
75
+ handleFatalError('Unhandled rejection', toError(reason), 'UNHANDLED_REJECTION');
74
76
  });
75
77
  try {
76
78
  if (isStdioMode) {
@@ -83,8 +85,9 @@ try {
83
85
  }
84
86
  }
85
87
  catch (error) {
86
- logError('Failed to start server', error instanceof Error ? error : undefined);
87
- const message = error instanceof Error ? error.message : String(error);
88
+ const resolvedError = toError(error);
89
+ logError('Failed to start server', resolvedError);
90
+ const { message } = resolvedError;
88
91
  process.stderr.write(`Failed to start server: ${message}\n`);
89
92
  process.exitCode = 1;
90
93
  scheduleForcedExit('Startup failure');
@@ -14,6 +14,7 @@ const IPV6_2002 = buildIpv6(['2002', 0, 0, 0, 0, 0, 0, 0]);
14
14
  const IPV6_FC00 = buildIpv6(['fc00', 0, 0, 0, 0, 0, 0, 0]);
15
15
  const IPV6_FE80 = buildIpv6(['fe80', 0, 0, 0, 0, 0, 0, 0]);
16
16
  const IPV6_FF00 = buildIpv6(['ff00', 0, 0, 0, 0, 0, 0, 0]);
17
+ const IPV6_MAPPED_PREFIX = '::ffff:';
17
18
  const BLOCKED_SUBNETS = [
18
19
  { subnet: buildIpv4([0, 0, 0, 0]), prefix: 8, family: 'ipv4' },
19
20
  { subnet: buildIpv4([10, 0, 0, 0]), prefix: 8, family: 'ipv4' },
@@ -42,10 +43,9 @@ export function createDefaultBlockList() {
42
43
  return list;
43
44
  }
44
45
  function extractMappedIpv4(ip) {
45
- const prefix = '::ffff:';
46
- if (!ip.startsWith(prefix))
46
+ if (!ip.startsWith(IPV6_MAPPED_PREFIX))
47
47
  return null;
48
- const mapped = ip.slice(prefix.length);
48
+ const mapped = ip.slice(IPV6_MAPPED_PREFIX.length);
49
49
  return isIP(mapped) === 4 ? mapped : null;
50
50
  }
51
51
  function stripIpv6ZoneId(ip) {
package/dist/json.js CHANGED
@@ -13,14 +13,15 @@ function processValue(obj, depth, seen) {
13
13
  }
14
14
  seen.add(obj);
15
15
  try {
16
+ const processChild = (value) => processValue(value, depth + 1, seen);
16
17
  if (Array.isArray(obj)) {
17
- return obj.map((item) => processValue(item, depth + 1, seen));
18
+ return obj.map((item) => processChild(item));
18
19
  }
19
20
  const keys = Object.keys(obj).sort((a, b) => a.localeCompare(b));
20
21
  const record = obj;
21
22
  const sortedObj = {};
22
23
  for (const key of keys) {
23
- sortedObj[key] = processValue(record[key], depth + 1, seen);
24
+ sortedObj[key] = processChild(record[key]);
24
25
  }
25
26
  return sortedObj;
26
27
  }
@@ -118,11 +118,7 @@ function isBashLine(line) {
118
118
  return false;
119
119
  }
120
120
  function detectBashIndicators(lines) {
121
- for (const line of lines) {
122
- if (isBashLine(line))
123
- return true;
124
- }
125
- return false;
121
+ return lines.some((line) => isBashLine(line));
126
122
  }
127
123
  function detectCssStructure(lines) {
128
124
  for (const line of lines) {
@@ -50,12 +50,14 @@ function throwIfAborted(signal, url, stage) {
50
50
  function getLineEnding(content) {
51
51
  return content.includes('\r\n') ? '\r\n' : '\n';
52
52
  }
53
+ function isBlank(line) {
54
+ return line === undefined || line.trim().length === 0;
55
+ }
53
56
  function hasFollowingContent(lines, startIndex) {
54
57
  // Optimization: Bound lookahead to avoid checking too many lines in huge files
55
58
  const max = Math.min(lines.length, startIndex + 50);
56
59
  for (let i = startIndex + 1; i < max; i++) {
57
- const line = lines[i];
58
- if (line && line.trim().length > 0)
60
+ if (!isBlank(lines[i]))
59
61
  return true;
60
62
  }
61
63
  return false;
@@ -145,11 +147,12 @@ function getTocBlockStats(lines, headingIndex) {
145
147
  function skipTocLines(lines, startIndex) {
146
148
  for (let i = startIndex; i < lines.length; i++) {
147
149
  const line = lines[i];
148
- if (!line)
150
+ if (line === undefined)
149
151
  continue;
150
- if (line.trim().length === 0)
152
+ const trimmed = line.trim();
153
+ if (!trimmed)
151
154
  continue;
152
- if (!REGEX.TOC_LINK.test(line))
155
+ if (!REGEX.TOC_LINK.test(trimmed))
153
156
  return i;
154
157
  }
155
158
  return lines.length;
@@ -13,17 +13,16 @@ export function isJsonRpcBatchRequest(body) {
13
13
  export function isMcpRequestBody(body) {
14
14
  return mcpRequestSchema.safeParse(body).success;
15
15
  }
16
- export function acceptsEventStream(header) {
16
+ function parseAcceptHeader(header) {
17
17
  if (!header)
18
- return false;
19
- return header
20
- .split(',')
21
- .some((value) => value.trim().toLowerCase().startsWith('text/event-stream'));
18
+ return [];
19
+ return header.split(',').map((value) => value.trim());
20
+ }
21
+ export function acceptsEventStream(header) {
22
+ return parseAcceptHeader(header).some((value) => value.trim().toLowerCase().startsWith('text/event-stream'));
22
23
  }
23
24
  function hasAcceptedMediaType(header, exact, wildcardPrefix) {
24
- if (!header)
25
- return false;
26
- return header.split(',').some((rawPart) => {
25
+ return parseAcceptHeader(header).some((rawPart) => {
27
26
  const mediaType = rawPart.trim().split(';', 1)[0]?.trim().toLowerCase();
28
27
  if (!mediaType)
29
28
  return false;
package/dist/mcp.js CHANGED
@@ -8,26 +8,35 @@ import { isObject } from './type-guards.js';
8
8
  /* -------------------------------------------------------------------------------------------------
9
9
  * Tasks API schemas
10
10
  * ------------------------------------------------------------------------------------------------- */
11
- const TaskGetSchema = z.strictObject({
11
+ const TaskGetSchema = z
12
+ .object({
12
13
  method: z.literal('tasks/get'),
13
- params: z.strictObject({ taskId: z.string() }),
14
- });
15
- const TaskListSchema = z.strictObject({
14
+ params: z.object({ taskId: z.string() }).loose(),
15
+ })
16
+ .loose();
17
+ const TaskListSchema = z
18
+ .object({
16
19
  method: z.literal('tasks/list'),
17
20
  params: z
18
- .strictObject({
21
+ .object({
19
22
  cursor: z.string().optional(),
20
23
  })
24
+ .loose()
21
25
  .optional(),
22
- });
23
- const TaskCancelSchema = z.strictObject({
26
+ })
27
+ .loose();
28
+ const TaskCancelSchema = z
29
+ .object({
24
30
  method: z.literal('tasks/cancel'),
25
- params: z.strictObject({ taskId: z.string() }),
26
- });
27
- const TaskResultSchema = z.strictObject({
31
+ params: z.object({ taskId: z.string() }).loose(),
32
+ })
33
+ .loose();
34
+ const TaskResultSchema = z
35
+ .object({
28
36
  method: z.literal('tasks/result'),
29
- params: z.strictObject({ taskId: z.string() }),
30
- });
37
+ params: z.object({ taskId: z.string() }).loose(),
38
+ })
39
+ .loose();
31
40
  const MIN_TASK_TTL_MS = 1_000;
32
41
  const MAX_TASK_TTL_MS = 86_400_000;
33
42
  const ExtendedCallToolRequestSchema = z
@@ -71,6 +80,14 @@ function parseExtendedCallToolRequest(request) {
71
80
  function isRecord(value) {
72
81
  return isObject(value);
73
82
  }
83
+ function normalizeSendNotification(sendNotification) {
84
+ if (typeof sendNotification !== 'function')
85
+ return undefined;
86
+ const notify = sendNotification;
87
+ return async (notification) => {
88
+ await Promise.resolve(notify(notification));
89
+ };
90
+ }
74
91
  function parseHandlerExtra(extra) {
75
92
  if (!isObject(extra))
76
93
  return undefined;
@@ -93,11 +110,9 @@ function parseHandlerExtra(extra) {
93
110
  if (typeof requestId === 'string' || typeof requestId === 'number') {
94
111
  parsed.requestId = requestId;
95
112
  }
96
- if (typeof sendNotification === 'function') {
97
- const notify = sendNotification;
98
- parsed.sendNotification = async (notification) => {
99
- await Promise.resolve(notify(notification));
100
- };
113
+ const normalizedSendNotification = normalizeSendNotification(sendNotification);
114
+ if (normalizedSendNotification) {
115
+ parsed.sendNotification = normalizedSendNotification;
101
116
  }
102
117
  return parsed;
103
118
  }
@@ -128,6 +128,13 @@ function mapToMcpLevel(level) {
128
128
  return 'info';
129
129
  }
130
130
  }
131
+ function resolveErrorText(err) {
132
+ if (err instanceof Error)
133
+ return err.message;
134
+ if (typeof err === 'string')
135
+ return err;
136
+ return 'unknown error';
137
+ }
131
138
  function safeWriteStderr(line) {
132
139
  if (!stderrAvailable)
133
140
  return;
@@ -164,20 +171,14 @@ function writeLog(level, message, meta) {
164
171
  .catch((err) => {
165
172
  if (!isDebugEnabled())
166
173
  return;
167
- let errorText = 'unknown error';
168
- if (err instanceof Error) {
169
- errorText = err.message;
170
- }
171
- else if (typeof err === 'string') {
172
- errorText = err;
173
- }
174
+ const errorText = resolveErrorText(err);
174
175
  safeWriteStderr(`[${createTimestamp()}] WARN: Failed to forward log to MCP${sessionId ? ` (sessionId=${sessionId})` : ''}: ${errorText}\n`);
175
176
  });
176
177
  }
177
178
  catch (err) {
178
179
  if (!isDebugEnabled())
179
180
  return;
180
- const errorText = err instanceof Error ? err.message : 'unknown error';
181
+ const errorText = resolveErrorText(err);
181
182
  safeWriteStderr(`[${createTimestamp()}] WARN: Failed to forward log to MCP (sync error): ${errorText}\n`);
182
183
  }
183
184
  }
package/dist/prompts.js CHANGED
@@ -1,18 +1,10 @@
1
1
  export function registerGetHelpPrompt(server, instructions, iconInfo) {
2
2
  const description = 'Return the Fetch URL usage instructions.';
3
+ const iconConfig = iconInfo ? { icons: [{ ...iconInfo }] } : {};
3
4
  server.registerPrompt('get-help', {
4
5
  title: 'Get Help',
5
6
  description,
6
- ...(iconInfo
7
- ? {
8
- icons: [
9
- {
10
- src: iconInfo.src,
11
- mimeType: iconInfo.mimeType,
12
- },
13
- ],
14
- }
15
- : {}),
7
+ ...iconConfig,
16
8
  }, () => ({
17
9
  description,
18
10
  messages: [
package/dist/resources.js CHANGED
@@ -21,17 +21,19 @@ function decodeSegment(value) {
21
21
  return value;
22
22
  }
23
23
  }
24
+ function trimToValue(value) {
25
+ const trimmed = value.trim();
26
+ return trimmed.length > 0 ? trimmed : undefined;
27
+ }
24
28
  function firstVariableValue(value) {
25
29
  if (typeof value === 'string') {
26
- const trimmed = value.trim();
27
- return trimmed.length > 0 ? trimmed : undefined;
30
+ return trimToValue(value);
28
31
  }
29
32
  if (Array.isArray(value)) {
30
33
  const first = value[0];
31
34
  if (typeof first !== 'string')
32
35
  return undefined;
33
- const trimmed = first.trim();
34
- return trimmed.length > 0 ? trimmed : undefined;
36
+ return trimToValue(first);
35
37
  }
36
38
  return undefined;
37
39
  }
@@ -1,23 +1,28 @@
1
1
  import { config } from './config.js';
2
2
  import { logDebug, logWarn } from './observability.js';
3
3
  const DROP_LOG_INTERVAL_MS = 10_000;
4
+ function setIfDefined(value, setter) {
5
+ if (value === undefined)
6
+ return;
7
+ setter(value);
8
+ }
4
9
  export function applyHttpServerTuning(server) {
5
10
  const { headersTimeoutMs, requestTimeoutMs, keepAliveTimeoutMs, keepAliveTimeoutBufferMs, maxHeadersCount, maxConnections, } = config.server.http;
6
- if (headersTimeoutMs !== undefined) {
7
- server.headersTimeout = headersTimeoutMs;
8
- }
9
- if (requestTimeoutMs !== undefined) {
10
- server.requestTimeout = requestTimeoutMs;
11
- }
12
- if (keepAliveTimeoutMs !== undefined) {
13
- server.keepAliveTimeout = keepAliveTimeoutMs;
14
- }
15
- if (keepAliveTimeoutBufferMs !== undefined) {
16
- server.keepAliveTimeoutBuffer = keepAliveTimeoutBufferMs;
17
- }
18
- if (maxHeadersCount !== undefined) {
19
- server.maxHeadersCount = maxHeadersCount;
20
- }
11
+ setIfDefined(headersTimeoutMs, (value) => {
12
+ server.headersTimeout = value;
13
+ });
14
+ setIfDefined(requestTimeoutMs, (value) => {
15
+ server.requestTimeout = value;
16
+ });
17
+ setIfDefined(keepAliveTimeoutMs, (value) => {
18
+ server.keepAliveTimeout = value;
19
+ });
20
+ setIfDefined(keepAliveTimeoutBufferMs, (value) => {
21
+ server.keepAliveTimeoutBuffer = value;
22
+ });
23
+ setIfDefined(maxHeadersCount, (value) => {
24
+ server.maxHeadersCount = value;
25
+ });
21
26
  if (typeof maxConnections === 'number' && maxConnections > 0) {
22
27
  server.maxConnections = maxConnections;
23
28
  if (typeof server.on === 'function') {
package/dist/server.js CHANGED
@@ -70,6 +70,9 @@ function createServerInfo(icons) {
70
70
  ...(icons ? { icons } : {}),
71
71
  };
72
72
  }
73
+ function toIconList(icon) {
74
+ return icon ? [icon] : undefined;
75
+ }
73
76
  /* -------------------------------------------------------------------------------------------------
74
77
  * Server lifecycle
75
78
  * ------------------------------------------------------------------------------------------------- */
@@ -88,14 +91,7 @@ async function createMcpServerWithOptions(options) {
88
91
  if (serverInstructions) {
89
92
  serverConfig.instructions = serverInstructions;
90
93
  }
91
- const serverInfo = createServerInfo(localIcon
92
- ? [
93
- {
94
- src: localIcon.src,
95
- mimeType: localIcon.mimeType,
96
- },
97
- ]
98
- : undefined);
94
+ const serverInfo = createServerInfo(toIconList(localIcon));
99
95
  const server = new McpServer(serverInfo, serverConfig);
100
96
  if (options?.registerObservabilityServer ?? true) {
101
97
  setMcpServer(server);
package/dist/session.js CHANGED
@@ -96,6 +96,9 @@ function moveSessionToEnd(sessions, sessionId, session) {
96
96
  sessions.delete(sessionId);
97
97
  sessions.set(sessionId, session);
98
98
  }
99
+ function isBlankSessionId(sessionId) {
100
+ return sessionId.length === 0;
101
+ }
99
102
  class InMemorySessionStore {
100
103
  sessionTtlMs;
101
104
  sessions = new Map();
@@ -104,12 +107,12 @@ class InMemorySessionStore {
104
107
  this.sessionTtlMs = sessionTtlMs;
105
108
  }
106
109
  get(sessionId) {
107
- if (!sessionId)
110
+ if (isBlankSessionId(sessionId))
108
111
  return undefined;
109
112
  return this.sessions.get(sessionId);
110
113
  }
111
114
  touch(sessionId) {
112
- if (!sessionId)
115
+ if (isBlankSessionId(sessionId))
113
116
  return;
114
117
  const session = this.sessions.get(sessionId);
115
118
  if (!session)
@@ -118,12 +121,12 @@ class InMemorySessionStore {
118
121
  moveSessionToEnd(this.sessions, sessionId, session);
119
122
  }
120
123
  set(sessionId, entry) {
121
- if (!sessionId)
124
+ if (isBlankSessionId(sessionId))
122
125
  return;
123
126
  moveSessionToEnd(this.sessions, sessionId, entry);
124
127
  }
125
128
  remove(sessionId) {
126
- if (!sessionId)
129
+ if (isBlankSessionId(sessionId))
127
130
  return undefined;
128
131
  const session = this.sessions.get(sessionId);
129
132
  this.sessions.delete(sessionId);
package/dist/tasks.js CHANGED
@@ -41,7 +41,7 @@ class TaskManager {
41
41
  removeExpiredTasks() {
42
42
  const now = Date.now();
43
43
  for (const [id, task] of this.tasks) {
44
- if (now - task._createdAtMs > task.ttl) {
44
+ if (this.isExpired(task, now)) {
45
45
  this.tasks.delete(id);
46
46
  }
47
47
  }
@@ -146,7 +146,7 @@ class TaskManager {
146
146
  for (const task of this.tasks.values()) {
147
147
  if (task.ownerKey !== ownerKey)
148
148
  continue;
149
- if (now - task._createdAtMs > task.ttl) {
149
+ if (this.isExpired(task, now)) {
150
150
  this.tasks.delete(task.taskId);
151
151
  continue;
152
152
  }
@@ -284,8 +284,8 @@ class TaskManager {
284
284
  for (const waiter of waiters)
285
285
  waiter(task);
286
286
  }
287
- isExpired(task) {
288
- return Date.now() - task._createdAtMs > task.ttl;
287
+ isExpired(task, now = Date.now()) {
288
+ return now - task._createdAtMs > task.ttl;
289
289
  }
290
290
  encodeCursor(index) {
291
291
  return Buffer.from(String(index), 'utf8').toString('base64url');
@@ -1,12 +1,15 @@
1
1
  import { setTimeout as setTimeoutPromise } from 'node:timers/promises';
2
2
  import { isError } from './type-guards.js';
3
+ function isAbortError(error) {
4
+ return isError(error) && error.name === 'AbortError';
5
+ }
3
6
  export function createUnrefTimeout(timeoutMs, value) {
4
7
  const controller = new AbortController();
5
8
  const promise = setTimeoutPromise(timeoutMs, value, {
6
9
  ref: false,
7
10
  signal: controller.signal,
8
11
  }).catch((err) => {
9
- if (isError(err) && err.name === 'AbortError') {
12
+ if (isAbortError(err)) {
10
13
  return new Promise(() => { });
11
14
  }
12
15
  throw err;
package/dist/tools.js CHANGED
@@ -708,10 +708,7 @@ function serializeMarkdownResult(result) {
708
708
  function buildStructuredContent(pipeline, inlineResult, inputUrl) {
709
709
  const cacheResourceUri = resolveCacheResourceUri(pipeline.cacheKey);
710
710
  const truncated = inlineResult.truncated ?? pipeline.data.truncated;
711
- let markdown = inlineResult.content;
712
- if (pipeline.data.truncated && typeof markdown === 'string') {
713
- markdown = appendTruncationMarker(markdown, TRUNCATION_MARKER);
714
- }
711
+ const markdown = applyTruncationMarker(inlineResult.content, pipeline.data.truncated);
715
712
  const { metadata } = pipeline.data;
716
713
  return {
717
714
  url: pipeline.originalUrl ?? pipeline.url,
@@ -728,6 +725,11 @@ function buildStructuredContent(pipeline, inlineResult, inputUrl) {
728
725
  ...(truncated ? { truncated: true } : {}),
729
726
  };
730
727
  }
728
+ function applyTruncationMarker(content, truncated) {
729
+ if (!truncated || typeof content !== 'string')
730
+ return content;
731
+ return appendTruncationMarker(content, TRUNCATION_MARKER);
732
+ }
731
733
  function resolveCacheResourceUri(cacheKey) {
732
734
  if (!cacheKey)
733
735
  return undefined;
@@ -864,14 +866,14 @@ function resolveSessionIdFromExtra(extra) {
864
866
  export function registerTools(server) {
865
867
  if (!config.tools.enabled.includes(FETCH_URL_TOOL_NAME))
866
868
  return;
867
- server.registerTool(TOOL_DEFINITION.name, {
869
+ const registeredTool = server.registerTool(TOOL_DEFINITION.name, {
868
870
  title: TOOL_DEFINITION.title,
869
871
  description: TOOL_DEFINITION.description,
870
872
  inputSchema: TOOL_DEFINITION.inputSchema,
871
873
  outputSchema: TOOL_DEFINITION.outputSchema,
872
874
  annotations: TOOL_DEFINITION.annotations,
873
875
  execution: TOOL_DEFINITION.execution,
874
- // Use specific tool icon here
875
876
  icons: [TOOL_ICON],
876
877
  }, withRequestContextIfMissing(TOOL_DEFINITION.handler));
878
+ registeredTool.execution = TOOL_DEFINITION.execution;
877
879
  }
package/dist/transform.js CHANGED
@@ -42,6 +42,9 @@ function getTagName(node) {
42
42
  const raw = node.tagName;
43
43
  return typeof raw === 'string' ? raw.toUpperCase() : '';
44
44
  }
45
+ function asError(value) {
46
+ return value instanceof Error ? value : undefined;
47
+ }
45
48
  function getAbortReason(signal) {
46
49
  const record = isObject(signal) ? signal : null;
47
50
  return record && 'reason' in record ? record['reason'] : undefined;
@@ -513,7 +516,7 @@ function extractArticle(document, url, signal) {
513
516
  };
514
517
  }
515
518
  catch (error) {
516
- logError('Failed to extract article with Readability', error instanceof Error ? error : undefined);
519
+ logError('Failed to extract article with Readability', asError(error));
517
520
  return null;
518
521
  }
519
522
  }
@@ -573,7 +576,7 @@ function extractContentContext(html, url, options) {
573
576
  if (error instanceof FetchError)
574
577
  throw error;
575
578
  abortPolicy.throwIfAborted(options.signal, url, 'extract:error');
576
- logError('Failed to extract content', error instanceof Error ? error : undefined);
579
+ logError('Failed to extract content', asError(error));
577
580
  const { document } = parseHTML('<html></html>');
578
581
  return { article: null, metadata: {}, document };
579
582
  }
@@ -1177,7 +1180,7 @@ export function htmlToMarkdown(html, metadata, options) {
1177
1180
  catch (error) {
1178
1181
  if (error instanceof FetchError)
1179
1182
  throw error;
1180
- logError('Failed to convert HTML to markdown', error instanceof Error ? error : undefined);
1183
+ logError('Failed to convert HTML to markdown', asError(error));
1181
1184
  throw new FetchError('Failed to convert HTML to markdown', url, 500, {
1182
1185
  reason: 'markdown_convert_failed',
1183
1186
  });
@@ -2,9 +2,9 @@ export function isObject(value) {
2
2
  return typeof value === 'object' && value !== null && !Array.isArray(value);
3
3
  }
4
4
  export function isError(value) {
5
- const ErrorConstructor = Error;
6
- if (typeof ErrorConstructor.isError === 'function') {
7
- return ErrorConstructor.isError(value);
5
+ const { isError: isErrorFn } = Error;
6
+ if (typeof isErrorFn === 'function') {
7
+ return isErrorFn(value);
8
8
  }
9
9
  return value instanceof Error;
10
10
  }
@@ -4,11 +4,15 @@ import { transformHtmlToMarkdownInProcess } from '../transform.js';
4
4
  const send = process.send?.bind(process);
5
5
  if (!send)
6
6
  throw new Error('transform-child started without IPC channel');
7
+ const sendMessage = send;
8
+ function postMessage(message) {
9
+ sendMessage(message);
10
+ }
7
11
  const controllersById = new Map();
8
12
  const decoder = new TextDecoder('utf-8');
9
13
  function postError(id, url, error) {
10
14
  if (error instanceof FetchError) {
11
- send?.({
15
+ postMessage({
12
16
  type: 'error',
13
17
  id,
14
18
  error: {
@@ -21,7 +25,7 @@ function postError(id, url, error) {
21
25
  });
22
26
  return;
23
27
  }
24
- send?.({
28
+ postMessage({
25
29
  type: 'error',
26
30
  id,
27
31
  error: {
@@ -52,7 +56,7 @@ function isValidMessage(msg) {
52
56
  return true;
53
57
  }
54
58
  function postValidationError(id, url, message) {
55
- send?.({
59
+ postMessage({
56
60
  type: 'error',
57
61
  id,
58
62
  error: { name: 'ValidationError', message, url },
@@ -94,7 +98,7 @@ function handleTransform(msg) {
94
98
  ...(inputTruncated ? { inputTruncated: true } : {}),
95
99
  });
96
100
  const { markdown, metadata, title, truncated } = result;
97
- send?.({
101
+ postMessage({
98
102
  type: 'result',
99
103
  id,
100
104
  result: title === undefined
@@ -61,6 +61,9 @@ function decodeHtmlBuffer(htmlBuffer, encoding) {
61
61
  return decoder.decode(htmlBuffer);
62
62
  }
63
63
  }
64
+ function resolveHtmlContent(html, htmlBuffer, encoding) {
65
+ return htmlBuffer ? decodeHtmlBuffer(htmlBuffer, encoding) : (html ?? '');
66
+ }
64
67
  function handleTransform(msg) {
65
68
  if (!isValidMessage(msg))
66
69
  return;
@@ -76,9 +79,7 @@ function handleTransform(msg) {
76
79
  const controller = new AbortController();
77
80
  controllersById.set(id, controller);
78
81
  try {
79
- const content = htmlBuffer
80
- ? decodeHtmlBuffer(htmlBuffer, encoding)
81
- : (html ?? '');
82
+ const content = resolveHtmlContent(html, htmlBuffer, encoding);
82
83
  const result = transformHtmlToMarkdownInProcess(content, url, {
83
84
  includeMetadata,
84
85
  signal: controller.signal,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@j0hanz/fetch-url-mcp",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "mcpName": "io.github.j0hanz/fetch-url-mcp",
5
5
  "description": "Intelligent web content fetcher MCP server that converts HTML to clean, AI-readable Markdown",
6
6
  "type": "module",