@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/tools.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
- import type { CallToolResult, ContentBlock } from '@modelcontextprotocol/sdk/types.js';
3
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
4
4
  import type { MarkdownTransformResult } from './transform-types.js';
5
5
  export interface FetchUrlInput {
6
6
  url: string;
@@ -8,12 +8,7 @@ export interface FetchUrlInput {
8
8
  forceRefresh?: boolean | undefined;
9
9
  maxInlineChars?: number | undefined;
10
10
  }
11
- export interface ToolContentBlock {
12
- type: 'text';
13
- text: string;
14
- }
15
- export type ToolContentBlockUnion = ContentBlock;
16
- export type ToolErrorResponse = CallToolResult & {
11
+ type ToolErrorResponse = CallToolResult & {
17
12
  structuredContent: {
18
13
  error: string;
19
14
  url: string;
@@ -22,10 +17,10 @@ export type ToolErrorResponse = CallToolResult & {
22
17
  };
23
18
  isError: true;
24
19
  };
25
- export type ToolResponseBase = CallToolResult & {
20
+ type ToolResponseBase = CallToolResult & {
26
21
  structuredContent: Record<string, unknown>;
27
22
  };
28
- export interface FetchPipelineOptions<T> {
23
+ interface FetchPipelineOptions<T> {
29
24
  url: string;
30
25
  cacheNamespace: string;
31
26
  signal?: AbortSignal;
@@ -39,7 +34,7 @@ export interface FetchPipelineOptions<T> {
39
34
  serialize?: (result: T) => string;
40
35
  deserialize?: (cached: string) => T | undefined;
41
36
  }
42
- export interface PipelineResult<T> {
37
+ interface PipelineResult<T> {
43
38
  data: T;
44
39
  fromCache: boolean;
45
40
  url: string;
@@ -48,8 +43,8 @@ export interface PipelineResult<T> {
48
43
  fetchedAt: string;
49
44
  cacheKey?: string | null;
50
45
  }
51
- export type ProgressToken = string | number;
52
- export interface RequestMeta {
46
+ type ProgressToken = string | number;
47
+ interface RequestMeta {
53
48
  progressToken?: ProgressToken | undefined;
54
49
  [key: string]: unknown;
55
50
  }
@@ -64,7 +59,7 @@ export interface ProgressNotification {
64
59
  method: 'notifications/progress';
65
60
  params: ProgressNotificationParams;
66
61
  }
67
- export interface ToolHandlerExtra {
62
+ interface ToolHandlerExtra {
68
63
  signal?: AbortSignal;
69
64
  requestId?: string | number;
70
65
  sessionId?: unknown;
@@ -89,7 +84,7 @@ interface InlineContentResult {
89
84
  contentSize: number;
90
85
  truncated?: boolean;
91
86
  }
92
- export type InlineResult = ReturnType<InlineContentLimiter['apply']>;
87
+ type InlineResult = ReturnType<InlineContentLimiter['apply']>;
93
88
  declare class InlineContentLimiter {
94
89
  apply(content: string, inlineLimitOverride?: number): InlineContentResult;
95
90
  private resolveInlineLimit;
package/dist/tools.js CHANGED
@@ -708,12 +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 &&
713
- !inlineResult.truncated &&
714
- typeof markdown === 'string') {
715
- markdown = appendTruncationMarker(markdown, TRUNCATION_MARKER);
716
- }
711
+ const markdown = applyTruncationMarker(inlineResult.content, pipeline.data.truncated);
717
712
  const { metadata } = pipeline.data;
718
713
  return {
719
714
  url: pipeline.originalUrl ?? pipeline.url,
@@ -730,6 +725,11 @@ function buildStructuredContent(pipeline, inlineResult, inputUrl) {
730
725
  ...(truncated ? { truncated: true } : {}),
731
726
  };
732
727
  }
728
+ function applyTruncationMarker(content, truncated) {
729
+ if (!truncated || typeof content !== 'string')
730
+ return content;
731
+ return appendTruncationMarker(content, TRUNCATION_MARKER);
732
+ }
733
733
  function resolveCacheResourceUri(cacheKey) {
734
734
  if (!cacheKey)
735
735
  return undefined;
@@ -866,14 +866,14 @@ function resolveSessionIdFromExtra(extra) {
866
866
  export function registerTools(server) {
867
867
  if (!config.tools.enabled.includes(FETCH_URL_TOOL_NAME))
868
868
  return;
869
- server.registerTool(TOOL_DEFINITION.name, {
869
+ const registeredTool = server.registerTool(TOOL_DEFINITION.name, {
870
870
  title: TOOL_DEFINITION.title,
871
871
  description: TOOL_DEFINITION.description,
872
872
  inputSchema: TOOL_DEFINITION.inputSchema,
873
873
  outputSchema: TOOL_DEFINITION.outputSchema,
874
874
  annotations: TOOL_DEFINITION.annotations,
875
875
  execution: TOOL_DEFINITION.execution,
876
- // Use specific tool icon here
877
876
  icons: [TOOL_ICON],
878
877
  }, withRequestContextIfMissing(TOOL_DEFINITION.handler));
878
+ registeredTool.execution = TOOL_DEFINITION.execution;
879
879
  }
@@ -102,6 +102,10 @@ export interface TransformWorkerCancelMessage {
102
102
  type: 'cancel';
103
103
  id: string;
104
104
  }
105
+ export interface TransformWorkerCancelledMessage {
106
+ type: 'cancelled';
107
+ id: string;
108
+ }
105
109
  export interface TransformWorkerResultMessage {
106
110
  type: 'result';
107
111
  id: string;
@@ -123,4 +127,4 @@ export interface TransformWorkerErrorMessage {
123
127
  details?: Record<string, unknown>;
124
128
  };
125
129
  }
126
- export type TransformWorkerOutgoingMessage = TransformWorkerResultMessage | TransformWorkerErrorMessage;
130
+ export type TransformWorkerOutgoingMessage = TransformWorkerResultMessage | TransformWorkerErrorMessage | TransformWorkerCancelledMessage;
@@ -1,5 +1,5 @@
1
1
  import type { ExtractedArticle, ExtractedMetadata, ExtractionResult, MarkdownTransformResult, MetadataBlock, TransformOptions, TransformStageContext } from './transform-types.js';
2
- export interface StageBudget {
2
+ interface StageBudget {
3
3
  totalBudgetMs: number;
4
4
  elapsedMs: number;
5
5
  }
@@ -21,7 +21,7 @@ export declare function isExtractionSufficient(article: ExtractedArticle | null,
21
21
  export declare function determineContentExtractionSource(article: ExtractedArticle | null): article is ExtractedArticle;
22
22
  export declare function createContentMetadataBlock(url: string, article: ExtractedArticle | null, extractedMeta: ExtractedMetadata, shouldExtractFromArticle: boolean, includeMetadata: boolean): MetadataBlock | undefined;
23
23
  export declare function transformHtmlToMarkdownInProcess(html: string, url: string, options: TransformOptions): MarkdownTransformResult;
24
- export interface TransformPoolStats {
24
+ interface TransformPoolStats {
25
25
  queueDepth: number;
26
26
  activeWorkers: number;
27
27
  capacity: number;
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
  }
@@ -1149,7 +1152,7 @@ function translateHtmlToMarkdown(params) {
1149
1152
  abortPolicy.throwIfAborted(signal, url, 'markdown:cleaned');
1150
1153
  const content = stageTracker.run(url, 'markdown:translate', () => translateHtmlFragmentToMarkdown(cleanedHtml));
1151
1154
  abortPolicy.throwIfAborted(signal, url, 'markdown:translated');
1152
- const cleaned = cleanupMarkdownArtifacts(content);
1155
+ const cleaned = cleanupMarkdownArtifacts(content, signal ? { signal, url } : { url });
1153
1156
  return url ? resolveRelativeUrls(cleaned, url) : cleaned;
1154
1157
  }
1155
1158
  function appendMetadataFooter(content, metadata, url) {
@@ -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
  });
@@ -1647,6 +1650,9 @@ function isWorkerResponse(raw) {
1647
1650
  if (raw['type'] === 'error') {
1648
1651
  return isWorkerErrorPayload(raw['error']);
1649
1652
  }
1653
+ if (raw['type'] === 'cancelled') {
1654
+ return true;
1655
+ }
1650
1656
  return false;
1651
1657
  }
1652
1658
  function createTaskContext() {
@@ -1836,6 +1842,7 @@ class WorkerPool {
1836
1842
  queue = [];
1837
1843
  queueHead = 0;
1838
1844
  inflight = new Map();
1845
+ cancelAcks = new Map();
1839
1846
  timeoutMs;
1840
1847
  queueMax;
1841
1848
  spawnWorkerImpl;
@@ -1963,7 +1970,7 @@ class WorkerPool {
1963
1970
  }
1964
1971
  const inflight = this.inflight.get(id);
1965
1972
  if (inflight) {
1966
- this.abortInflight(id, url, inflight.workerIndex);
1973
+ void this.abortInflight(id, url, inflight.workerIndex);
1967
1974
  return;
1968
1975
  }
1969
1976
  const queuedIndex = this.findQueuedIndex(id);
@@ -1985,8 +1992,36 @@ class WorkerPool {
1985
1992
  this.maybeCompactQueue();
1986
1993
  }
1987
1994
  }
1988
- abortInflight(id, url, workerIndex) {
1995
+ resolveCancelAck(id) {
1996
+ const pending = this.cancelAcks.get(id);
1997
+ if (!pending)
1998
+ return;
1999
+ pending.timeout.cancel();
2000
+ pending.resolve();
2001
+ }
2002
+ waitForCancelAck(id) {
2003
+ const existing = this.cancelAcks.get(id);
2004
+ if (existing) {
2005
+ return existing.promise;
2006
+ }
2007
+ let resolve = () => { };
2008
+ const timeout = createUnrefTimeout(200, undefined);
2009
+ const racePromise = new Promise((finish) => {
2010
+ resolve = finish;
2011
+ });
2012
+ const promise = Promise.race([racePromise, timeout.promise]).finally(() => {
2013
+ this.cancelAcks.delete(id);
2014
+ timeout.cancel();
2015
+ });
2016
+ this.cancelAcks.set(id, { promise, resolve, timeout });
2017
+ return promise;
2018
+ }
2019
+ async abortInflight(id, url, workerIndex) {
1989
2020
  const slot = this.workers[workerIndex];
2021
+ const inflight = this.inflight.get(id);
2022
+ if (inflight) {
2023
+ inflight.cancelPending = true;
2024
+ }
1990
2025
  if (slot) {
1991
2026
  try {
1992
2027
  slot.host.postMessage({ type: 'cancel', id });
@@ -1995,6 +2030,7 @@ class WorkerPool {
1995
2030
  // Worker may be unavailable; failure is acceptable during abort
1996
2031
  }
1997
2032
  }
2033
+ await this.waitForCancelAck(id);
1998
2034
  this.failTask(id, abortPolicy.createAbortError(url, 'transform:signal-abort'));
1999
2035
  if (slot)
2000
2036
  this.restartWorker(workerIndex, slot);
@@ -2059,6 +2095,15 @@ class WorkerPool {
2059
2095
  if (!isWorkerResponse(raw))
2060
2096
  return;
2061
2097
  const message = raw;
2098
+ if (message.type === 'cancelled') {
2099
+ this.resolveCancelAck(message.id);
2100
+ return;
2101
+ }
2102
+ const inflightPeek = this.inflight.get(message.id);
2103
+ if (inflightPeek?.cancelPending) {
2104
+ this.resolveCancelAck(message.id);
2105
+ return;
2106
+ }
2062
2107
  const inflight = this.takeInflight(message.id);
2063
2108
  if (!inflight)
2064
2109
  return;
@@ -2208,6 +2253,7 @@ class WorkerPool {
2208
2253
  abortListener: task.abortListener,
2209
2254
  workerIndex,
2210
2255
  context: task.context,
2256
+ cancelPending: false,
2211
2257
  });
2212
2258
  try {
2213
2259
  const { message, transferList } = buildWorkerDispatchPayload(task, slot.host.supportsTransferList);
@@ -1,6 +1,6 @@
1
1
  export declare function isObject(value: unknown): value is Record<PropertyKey, unknown>;
2
2
  export declare function isError(value: unknown): value is Error;
3
- export interface LikeNode {
3
+ interface LikeNode {
4
4
  readonly tagName?: string | undefined;
5
5
  readonly nodeName?: string | undefined;
6
6
  readonly nodeType?: number | undefined;
@@ -12,3 +12,4 @@ export interface LikeNode {
12
12
  getAttribute?(name: string): string | null;
13
13
  }
14
14
  export declare function isLikeNode(value: unknown): value is LikeNode;
15
+ export {};
@@ -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,
@@ -121,6 +122,7 @@ port.on('message', (raw) => {
121
122
  const controller = controllersById.get(id);
122
123
  if (controller)
123
124
  controller.abort(new Error('Canceled'));
125
+ port.postMessage({ type: 'cancelled', id });
124
126
  return;
125
127
  }
126
128
  if (type === 'transform') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@j0hanz/fetch-url-mcp",
3
- "version": "1.1.0",
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",