@j0hanz/fetch-url-mcp 1.1.0 → 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.d.ts CHANGED
@@ -5,22 +5,22 @@ declare const CachedPayloadSchema: z.ZodObject<{
5
5
  markdown: z.ZodOptional<z.ZodString>;
6
6
  title: z.ZodOptional<z.ZodString>;
7
7
  }, z.core.$strict>;
8
- export type CachedPayload = z.infer<typeof CachedPayloadSchema>;
9
- export interface CacheEntry {
8
+ type CachedPayload = z.infer<typeof CachedPayloadSchema>;
9
+ interface CacheEntry {
10
10
  url: string;
11
11
  title?: string;
12
12
  content: string;
13
13
  fetchedAt: string;
14
14
  expiresAt: string;
15
15
  }
16
- export interface CacheKeyParts {
16
+ interface CacheKeyParts {
17
17
  namespace: string;
18
18
  urlHash: string;
19
19
  }
20
- export interface CacheSetOptions {
20
+ interface CacheSetOptions {
21
21
  force?: boolean;
22
22
  }
23
- export interface CacheGetOptions {
23
+ interface CacheGetOptions {
24
24
  force?: boolean;
25
25
  }
26
26
  interface CacheEntryMetadata {
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.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export declare const serverVersion: string;
2
2
  export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
3
- export type TransformWorkerMode = 'threads' | 'process';
3
+ type TransformWorkerMode = 'threads' | 'process';
4
4
  type AuthMode = 'oauth' | 'static';
5
5
  interface WorkerResourceLimits {
6
6
  maxOldGenerationSizeMb?: number;
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.d.ts CHANGED
@@ -1,12 +1,12 @@
1
- export interface FetchOptions {
1
+ interface FetchOptions {
2
2
  signal?: AbortSignal;
3
3
  }
4
- export interface TransformResult {
4
+ interface TransformResult {
5
5
  readonly url: string;
6
6
  readonly transformed: boolean;
7
7
  readonly platform?: string;
8
8
  }
9
- export interface FetchTelemetryContext {
9
+ interface FetchTelemetryContext {
10
10
  requestId: string;
11
11
  startTime: number;
12
12
  url: string;
@@ -40,3 +40,4 @@ export declare function fetchNormalizedUrlBuffer(normalizedUrl: string, options?
40
40
  truncated: boolean;
41
41
  finalUrl: string;
42
42
  }>;
43
+ export {};
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) {
@@ -1,6 +1,11 @@
1
1
  import type { MetadataBlock } from './transform-types.js';
2
- export declare function cleanupMarkdownArtifacts(content: string): string;
2
+ interface CleanupOptions {
3
+ signal?: AbortSignal;
4
+ url?: string;
5
+ }
6
+ export declare function cleanupMarkdownArtifacts(content: string, options?: CleanupOptions): string;
3
7
  export declare function extractTitleFromRawMarkdown(content: string): string | undefined;
4
8
  export declare function addSourceToMarkdown(content: string, url: string): string;
5
9
  export declare function isRawTextContent(content: string): boolean;
6
10
  export declare function buildMetadataFooter(metadata?: MetadataBlock, fallbackUrl?: string): string;
11
+ export {};