@j0hanz/fetch-url-mcp 1.13.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +815 -807
  3. package/dist/assets/logo.svg +53 -53
  4. package/dist/http/auth.d.ts +7 -3
  5. package/dist/http/auth.d.ts.map +1 -1
  6. package/dist/http/auth.js +65 -37
  7. package/dist/http/helpers.d.ts +21 -0
  8. package/dist/http/helpers.d.ts.map +1 -0
  9. package/dist/http/helpers.js +31 -0
  10. package/dist/http/index.d.ts +1 -0
  11. package/dist/http/index.d.ts.map +1 -1
  12. package/dist/http/index.js +1 -0
  13. package/dist/http/native.d.ts +6 -24
  14. package/dist/http/native.d.ts.map +1 -1
  15. package/dist/http/native.js +141 -86
  16. package/dist/http/rate-limit.d.ts +1 -1
  17. package/dist/http/rate-limit.d.ts.map +1 -1
  18. package/dist/http/rate-limit.js +3 -9
  19. package/dist/index.js +0 -0
  20. package/dist/lib/config.d.ts +1 -0
  21. package/dist/lib/config.d.ts.map +1 -1
  22. package/dist/lib/config.js +1 -0
  23. package/dist/lib/core.d.ts +4 -3
  24. package/dist/lib/core.d.ts.map +1 -1
  25. package/dist/lib/core.js +4 -1
  26. package/dist/lib/error/classify.d.ts.map +1 -1
  27. package/dist/lib/error/classify.js +27 -6
  28. package/dist/lib/error/payload.d.ts +2 -2
  29. package/dist/lib/error/payload.d.ts.map +1 -1
  30. package/dist/lib/error/payload.js +2 -2
  31. package/dist/lib/mcp-interop.d.ts +9 -108
  32. package/dist/lib/mcp-interop.d.ts.map +1 -1
  33. package/dist/lib/mcp-interop.js +39 -240
  34. package/dist/lib/net/http.d.ts.map +1 -1
  35. package/dist/lib/net/http.js +4 -17
  36. package/dist/resources/index.d.ts +1 -1
  37. package/dist/resources/index.d.ts.map +1 -1
  38. package/dist/resources/index.js +67 -60
  39. package/dist/server.d.ts +1 -1
  40. package/dist/server.d.ts.map +1 -1
  41. package/dist/server.js +39 -34
  42. package/dist/tasks/adapter.d.ts +15 -0
  43. package/dist/tasks/adapter.d.ts.map +1 -0
  44. package/dist/tasks/adapter.js +138 -0
  45. package/dist/tasks/manager.d.ts +14 -82
  46. package/dist/tasks/manager.d.ts.map +1 -1
  47. package/dist/tasks/manager.js +48 -607
  48. package/dist/tasks/store.d.ts +33 -19
  49. package/dist/tasks/store.d.ts.map +1 -1
  50. package/dist/tasks/store.js +143 -80
  51. package/dist/tools/index.d.ts +7 -13
  52. package/dist/tools/index.d.ts.map +1 -1
  53. package/dist/tools/index.js +153 -58
  54. package/dist/transform/index.js +1 -1
  55. package/package.json +110 -108
@@ -1,15 +1,15 @@
1
- import { ErrorCode, } from '@modelcontextprotocol/sdk/types.js';
1
+ import { ProtocolErrorCode, } from '@modelcontextprotocol/server';
2
2
  import { config } from '../lib/config.js';
3
- import { Loggers, logInfo } from '../lib/core.js';
4
- import { classifyAndLogToolError, isAbortError } from '../lib/error/index.js';
5
- import { createMcpError, createProgressReporter, registerToolPresentation, validateOrThrow, } from '../lib/mcp-interop.js';
3
+ import { logError, Loggers, logInfo } from '../lib/core.js';
4
+ import { classifyAndLogToolError, FetchError, getErrorMessage, handleToolError, isAbortError, } from '../lib/error/index.js';
5
+ import { createProgressReporter, createProtocolError, validateOrThrow, } from '../lib/mcp-interop.js';
6
6
  import { finalizeInlineMarkdown, markdownTransform, performSharedFetch, withSignal, } from '../lib/net/index.js';
7
7
  import { composeAbortSignal, isObject } from '../lib/utils.js';
8
8
  import { fetchUrlInputSchema, fetchUrlOutputSchema, normalizeExtractedMetadata, normalizePageTitle, } from '../schemas.js';
9
- import { registerTaskCapableTool, setTaskCapableToolSupport, unregisterTaskCapableTool, withRequestContextIfMissing, } from '../tasks/index.js';
9
+ import { attachAbortController, detachAbortController, withRelatedTaskMeta, } from '../tasks/index.js';
10
10
  export const FETCH_URL_TOOL_NAME = 'fetch-url';
11
- const FETCH_URL_TOOL_DESCRIPTION = `
12
- Fetch public webpages and convert HTML to clean Markdown.
11
+ const FETCH_URL_TOOL_DESCRIPTION = `
12
+ Fetch public webpages and convert HTML to clean Markdown.
13
13
  `.trim();
14
14
  const TOOL_ICON = {
15
15
  src: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNMjEgMTV2NGEyIDIgMCAwIDEtMiAySDVhMiAyIDAgMCAxLTItMnYtNCIvPjxwb2x5bGluZSBwb2ludHM9IjcgMTAgMTIgMTUgMTcgMTAiLz48bGluZSB4MT0iMTIiIHkxPSIxNSIgeDI9IjEyIiB5Mj0iMyIvPjwvc3ZnPg==',
@@ -63,7 +63,7 @@ function buildStructuredContent(pipeline, inlineResult, inputUrl) {
63
63
  };
64
64
  }
65
65
  function validateStructuredContent(structuredContent) {
66
- validateOrThrow(fetchUrlOutputSchema, structuredContent, ErrorCode.InternalError, 'Output validation failed', Loggers.LOG_FETCH_URL);
66
+ validateOrThrow(fetchUrlOutputSchema, structuredContent, ProtocolErrorCode.InternalError, 'Output validation failed', Loggers.LOG_FETCH_URL);
67
67
  }
68
68
  export function buildFetchUrlContentBlocks(structuredContent) {
69
69
  const markdown = typeof structuredContent['markdown'] === 'string'
@@ -92,14 +92,16 @@ const Step = {
92
92
  DONE: 7,
93
93
  };
94
94
  function formatContentSize(contentSize) {
95
- if (contentSize < 1000)
96
- return `${contentSize} chars`;
97
- if (contentSize < 1_000_000)
98
- return `${(contentSize / 1024).toFixed(1)} KB`;
99
- return `${(contentSize / (1024 * 1024)).toFixed(1)} MB`;
95
+ if (contentSize < 1024)
96
+ return `${contentSize} B`;
97
+ if (contentSize < 1_048_576) {
98
+ const kb = contentSize / 1024;
99
+ return kb < 10 ? `${kb.toFixed(1)} KB` : `${kb.toFixed(0)} KB`;
100
+ }
101
+ return `${(contentSize / 1_048_576).toFixed(1)} MB`;
100
102
  }
101
103
  function buildFetchSuccessSummary(contentSize) {
102
- return `Done ${formatContentSize(contentSize)}`;
104
+ return `Fetch completed \u2022 ${formatContentSize(contentSize)}`;
103
105
  }
104
106
  export function getFetchCompletionStatusMessage(result) {
105
107
  if (!isObject(result))
@@ -115,25 +117,59 @@ export function getFetchCompletionStatusMessage(result) {
115
117
  export class FetchUrlProgressPlan {
116
118
  reporter;
117
119
  context;
120
+ onStatusMessage;
118
121
  total = Step.DONE;
119
- constructor(reporter, context) {
122
+ constructor(reporter, context, onStatusMessage) {
120
123
  this.reporter = reporter;
121
124
  this.context = context;
125
+ this.onStatusMessage = onStatusMessage;
122
126
  }
123
127
  reportStart() {
124
- this.reporter.report(Step.START, 'Preparing request', this.total);
128
+ const message = 'Preparing';
129
+ this.onStatusMessage?.(message);
130
+ this.reporter.report({
131
+ progress: Step.START,
132
+ message,
133
+ total: this.total,
134
+ });
125
135
  }
126
136
  reportStage(stage) {
127
137
  const mapped = this.mapStage(stage);
128
138
  if (!mapped)
129
139
  return;
130
- this.reporter.report(mapped.step, mapped.message, this.total);
140
+ this.onStatusMessage?.(mapped.message);
141
+ this.reporter.report({
142
+ progress: mapped.step,
143
+ message: mapped.message,
144
+ total: this.total,
145
+ });
131
146
  }
132
147
  reportSuccess(contentSize) {
133
- this.reporter.report(Step.DONE, buildFetchSuccessSummary(contentSize), this.total);
148
+ const message = buildFetchSuccessSummary(contentSize);
149
+ this.onStatusMessage?.(message);
150
+ this.reporter.report({
151
+ progress: Step.DONE,
152
+ message,
153
+ total: this.total,
154
+ });
134
155
  }
135
- reportFailure(cancelled) {
136
- this.reporter.report(Step.DONE, cancelled ? 'Cancelled' : 'Failed', this.total);
156
+ reportFailure(cancelled, error) {
157
+ let message;
158
+ if (cancelled) {
159
+ message = 'Cancelled';
160
+ }
161
+ else if (error instanceof FetchError && error.statusCode > 0) {
162
+ message = `Fetch failed \u2022 ${String(error.statusCode)}`;
163
+ }
164
+ else {
165
+ message = 'Fetch failed';
166
+ }
167
+ this.onStatusMessage?.(message);
168
+ this.reporter.report({
169
+ progress: Step.DONE,
170
+ message,
171
+ total: this.total,
172
+ });
137
173
  }
138
174
  mapStage(stage) {
139
175
  switch (stage) {
@@ -150,20 +186,23 @@ export class FetchUrlProgressPlan {
150
186
  case 'response_ready':
151
187
  return {
152
188
  step: Step.RESPONSE,
153
- message: 'Received response',
189
+ message: 'Processing response',
154
190
  };
155
191
  case 'transform_start':
156
192
  return {
157
193
  step: Step.TRANSFORM,
158
- message: 'Parsing HTML -> Markdown',
194
+ message: 'Converting to Markdown',
159
195
  };
160
196
  case 'prepare_output':
161
197
  return {
162
198
  step: Step.PREPARE,
163
- message: 'Fetch completed',
199
+ message: 'Finalizing',
164
200
  };
165
201
  case 'finalize_output':
166
- return undefined;
202
+ return {
203
+ step: Step.DONE,
204
+ message: 'Fetch completed',
205
+ };
167
206
  }
168
207
  }
169
208
  }
@@ -174,7 +213,7 @@ function buildToolAbortSignal(extraSignal) {
174
213
  const timeout = config.tools.timeoutMs > 0 ? config.tools.timeoutMs : HARD_TOOL_TIMEOUT_MS;
175
214
  const signal = composeAbortSignal(extraSignal, timeout);
176
215
  if (!signal) {
177
- throw createMcpError(ErrorCode.InternalError, 'Failed to create timeout signal');
216
+ throw createProtocolError(ProtocolErrorCode.InternalError, 'Failed to create timeout signal');
178
217
  }
179
218
  return signal;
180
219
  }
@@ -190,21 +229,35 @@ function buildFetchOptions(url, signal, progressPlan) {
190
229
  },
191
230
  };
192
231
  }
193
- async function executeFetch(input, extra) {
232
+ async function writeRequestScopedLog(ctx, level, message, data) {
233
+ if (!ctx)
234
+ return;
235
+ try {
236
+ await ctx.mcpReq.log(level, { message, ...data }, Loggers.LOG_FETCH_URL);
237
+ }
238
+ catch {
239
+ // Request-scoped logging is best-effort; internal logging remains authoritative.
240
+ }
241
+ }
242
+ async function executeFetch(input, ctx, executionSignal, onStatusMessage) {
194
243
  const { url } = input;
195
- const signal = buildToolAbortSignal(extra?.signal);
244
+ const mcpReq = ctx?.mcpReq;
245
+ const signal = buildToolAbortSignal(executionSignal ?? mcpReq?.signal);
196
246
  const startedAt = performance.now();
197
- const relatedTaskMeta = extra?._meta?.['modelcontextprotocol.io/related-task'];
198
- const relatedTask = isObject(relatedTaskMeta) ? relatedTaskMeta : undefined;
199
- const progressPlan = new FetchUrlProgressPlan(createProgressReporter(extra), formatUrlForDisplay(url));
247
+ const meta = mcpReq?._meta;
248
+ const progressToken = meta?.progressToken;
249
+ const taskId = ctx?.task?.id;
250
+ const progressPlan = new FetchUrlProgressPlan(createProgressReporter(ctx), formatUrlForDisplay(url), onStatusMessage);
200
251
  try {
201
252
  logInfo('fetch-url started', {
202
253
  inputUrl: url,
203
- hasProgressToken: extra?._meta?.progressToken !== undefined,
204
- ...(isObject(relatedTask) && typeof relatedTask['taskId'] === 'string'
205
- ? { taskId: relatedTask['taskId'] }
206
- : {}),
254
+ hasProgressToken: progressToken !== undefined,
255
+ ...(taskId ? { taskId } : {}),
207
256
  }, Loggers.LOG_FETCH_URL);
257
+ await writeRequestScopedLog(ctx, 'info', 'fetch-url started', {
258
+ inputUrl: url,
259
+ ...(taskId ? { taskId } : {}),
260
+ });
208
261
  progressPlan.reportStart();
209
262
  const { pipeline, inlineResult } = await performSharedFetch(buildFetchOptions(url, signal, progressPlan));
210
263
  const truncated = inlineResult.truncated ?? pipeline.data.truncated;
@@ -216,18 +269,30 @@ async function executeFetch(input, extra) {
216
269
  durationMs: Math.round(performance.now() - startedAt),
217
270
  ...(truncated ? { truncated: true } : {}),
218
271
  }, Loggers.LOG_FETCH_URL);
272
+ await writeRequestScopedLog(ctx, 'info', 'fetch-url completed', {
273
+ inputUrl: url,
274
+ resolvedUrl: pipeline.url,
275
+ contentSize: inlineResult.contentSize,
276
+ ...(taskId ? { taskId } : {}),
277
+ ...(truncated ? { truncated: true } : {}),
278
+ });
219
279
  const response = buildResponse(pipeline, inlineResult, url);
220
280
  progressPlan.reportSuccess(inlineResult.contentSize);
221
281
  return response;
222
282
  }
223
283
  catch (error) {
224
- progressPlan.reportFailure(isAbortError(error));
284
+ progressPlan.reportFailure(isAbortError(error), error);
285
+ await writeRequestScopedLog(ctx, isAbortError(error) ? 'warning' : 'error', 'fetch-url failed', {
286
+ inputUrl: url,
287
+ error: getErrorMessage(error),
288
+ ...(taskId ? { taskId } : {}),
289
+ });
225
290
  throw error;
226
291
  }
227
292
  }
228
- export async function fetchUrlToolHandler(input, extra) {
293
+ export async function fetchUrlToolHandler(input, ctx) {
229
294
  const startedAt = performance.now();
230
- return executeFetch(input, extra).catch((error) => {
295
+ return executeFetch(input, ctx).catch((error) => {
231
296
  const durationMs = Math.round(performance.now() - startedAt);
232
297
  return classifyAndLogToolError(error, { url: input.url, durationMs }, Loggers.LOG_FETCH_URL, 'fetch-url', 'Failed to fetch URL');
233
298
  });
@@ -249,42 +314,72 @@ const TOOL_DEFINITION = {
249
314
  openWorldHint: true,
250
315
  },
251
316
  };
252
- function createTaskCapableDescriptor() {
253
- return {
254
- name: TOOL_DEFINITION.name,
255
- parseArguments: (args) => {
256
- return validateOrThrow(TOOL_DEFINITION.inputSchema, args, ErrorCode.InvalidParams, 'Invalid parameters for fetch-url', Loggers.LOG_FETCH_URL);
257
- },
258
- execute: TOOL_DEFINITION.handler,
259
- getCompletionStatusMessage: getFetchCompletionStatusMessage,
260
- taskSupport: 'optional',
261
- };
262
- }
263
317
  export function registerTools(server) {
264
318
  if (!config.tools.enabled.includes(FETCH_URL_TOOL_NAME)) {
265
- unregisterTaskCapableTool(server, FETCH_URL_TOOL_NAME);
266
319
  return {
267
320
  setTaskSupport: () => { },
268
321
  };
269
322
  }
270
- const descriptor = createTaskCapableDescriptor();
271
- registerTaskCapableTool(server, descriptor);
272
- const registeredTool = server.registerTool(TOOL_DEFINITION.name, {
323
+ const registeredTool = server.experimental.tasks.registerToolTask(TOOL_DEFINITION.name, {
273
324
  title: TOOL_DEFINITION.title,
274
325
  description: TOOL_DEFINITION.description,
275
326
  inputSchema: TOOL_DEFINITION.inputSchema,
276
327
  outputSchema: TOOL_DEFINITION.outputSchema,
277
328
  annotations: TOOL_DEFINITION.annotations,
278
329
  execution: { taskSupport: 'optional' },
279
- icons: [TOOL_ICON],
280
- }, withRequestContextIfMissing(TOOL_DEFINITION.handler));
281
- registerToolPresentation(server, TOOL_DEFINITION.name, {
282
- icons: [TOOL_ICON],
330
+ _meta: { icons: [TOOL_ICON] },
331
+ }, {
332
+ createTask: async (args, ctx) => {
333
+ const task = await ctx.task.store.createTask(ctx.task.requestedTtl !== undefined
334
+ ? { ttl: ctx.task.requestedTtl }
335
+ : {});
336
+ const taskSignal = attachAbortController(task.taskId).signal;
337
+ const updateStatus = (message) => {
338
+ void ctx.task.store.updateTaskStatus(task.taskId, 'working', message);
339
+ };
340
+ // Spin off background execution
341
+ executeFetch(args, ctx, taskSignal, updateStatus)
342
+ .then(async (result) => {
343
+ try {
344
+ await ctx.task.store.storeTaskResult(task.taskId, 'completed', result);
345
+ }
346
+ catch (storeError) {
347
+ logError('Failed to store completed task result', {
348
+ taskId: task.taskId,
349
+ error: getErrorMessage(storeError),
350
+ }, Loggers.LOG_TASKS);
351
+ await ctx.task.store.updateTaskStatus(task.taskId, 'failed', 'Failed to store result');
352
+ }
353
+ })
354
+ .catch(async (error) => {
355
+ logError('Background execution crashed', { taskId: task.taskId, error: getErrorMessage(error) }, Loggers.LOG_TASKS);
356
+ const errorResult = handleToolError(error, args.url, 'Background execution failed');
357
+ try {
358
+ await ctx.task.store.storeTaskResult(task.taskId, 'failed', errorResult);
359
+ }
360
+ catch (storeError) {
361
+ logError('Failed to store task error result', {
362
+ taskId: task.taskId,
363
+ error: getErrorMessage(storeError),
364
+ }, Loggers.LOG_TASKS);
365
+ await ctx.task.store.updateTaskStatus(task.taskId, 'failed', getErrorMessage(error));
366
+ }
367
+ })
368
+ .finally(() => {
369
+ detachAbortController(task.taskId);
370
+ });
371
+ return { task };
372
+ },
373
+ getTask: async (_args, ctx) => {
374
+ return ctx.task.store.getTask(ctx.task.id);
375
+ },
376
+ getTaskResult: async (_args, ctx) => {
377
+ const result = (await ctx.task.store.getTaskResult(ctx.task.id));
378
+ return withRelatedTaskMeta(result, ctx.task.id);
379
+ },
283
380
  });
284
381
  const updateTaskSupport = (support) => {
285
- setTaskCapableToolSupport(server, FETCH_URL_TOOL_NAME, support);
286
382
  registeredTool.execution = { taskSupport: support };
287
383
  };
288
- updateTaskSupport('optional');
289
384
  return { setTaskSupport: updateTaskSupport };
290
385
  }
@@ -540,7 +540,7 @@ export function buildMetadataFooter(metadata, fallbackUrl) {
540
540
  if (parts.length > 0)
541
541
  lines.push(` ${parts.join(' | ')}`);
542
542
  if (metadata.description)
543
- lines.push(` <sub>${metadata.description}</sub>`);
543
+ lines.push(` ${metadata.description}`);
544
544
  return lines.join('\n');
545
545
  }
546
546
  // eslint-disable-next-line sonarjs/slow-regex -- bounded title separator, no user input
package/package.json CHANGED
@@ -1,108 +1,110 @@
1
- {
2
- "name": "@j0hanz/fetch-url-mcp",
3
- "version": "1.13.0",
4
- "mcpName": "io.github.j0hanz/fetch-url-mcp",
5
- "description": "An MCP server that fetches web pages and converts them to clean, readable Markdown.",
6
- "type": "module",
7
- "main": "./dist/index.js",
8
- "types": "./dist/index.d.ts",
9
- "bin": {
10
- "fetch-url-mcp": "dist/index.js"
11
- },
12
- "exports": {
13
- ".": {
14
- "types": "./dist/index.d.ts",
15
- "default": "./dist/index.js"
16
- },
17
- "./package.json": "./package.json"
18
- },
19
- "files": [
20
- "dist",
21
- "README.md"
22
- ],
23
- "repository": {
24
- "type": "git",
25
- "url": "https://github.com/j0hanz/fetch-url-mcp.git"
26
- },
27
- "homepage": "https://github.com/j0hanz/fetch-url-mcp#readme",
28
- "bugs": {
29
- "url": "https://github.com/j0hanz/fetch-url-mcp/issues"
30
- },
31
- "author": "j0hanz",
32
- "license": "MIT",
33
- "keywords": [
34
- "mcp",
35
- "mcp-server",
36
- "model-context-protocol",
37
- "web-fetching",
38
- "content-extraction",
39
- "readability",
40
- "markdown",
41
- "html-to-markdown",
42
- "web-scraper",
43
- "llm-context",
44
- "ai-tools",
45
- "fetch-url-mcp",
46
- "typescript",
47
- "nodejs",
48
- "stdio",
49
- "streamable-http",
50
- "structured-output",
51
- "metadata-extraction",
52
- "url-rewriting",
53
- "worker-threads",
54
- "content-cleanup"
55
- ],
56
- "scripts": {
57
- "clean": "node scripts/tasks.mjs clean",
58
- "build": "node scripts/tasks.mjs build",
59
- "copy:assets": "node scripts/tasks.mjs copy:assets",
60
- "prepare": "npm run build",
61
- "dev": "tsc --watch --preserveWatchOutput",
62
- "dev:run": "node --env-file=.env --watch dist/index.js",
63
- "start": "node dist/index.js",
64
- "format": "prettier --write .",
65
- "type-check": "node scripts/tasks.mjs type-check",
66
- "type-check:src": "node node_modules/typescript/bin/tsc -p tsconfig.json --noEmit",
67
- "type-check:tests": "node node_modules/typescript/bin/tsc -p tsconfig.test.json --noEmit",
68
- "type-check:diagnostics": "tsc --noEmit --extendedDiagnostics",
69
- "type-check:trace": "node -e \"require('fs').rmSync('.ts-trace',{recursive:true,force:true})\" && tsc --noEmit --generateTrace .ts-trace",
70
- "lint": "eslint .",
71
- "lint:tests": "eslint __tests__",
72
- "lint:fix": "eslint . --fix",
73
- "test": "node scripts/tasks.mjs test",
74
- "test:coverage": "node scripts/tasks.mjs test --coverage",
75
- "knip": "knip",
76
- "knip:fix": "knip --fix",
77
- "inspector": "npm run build && npx -y @modelcontextprotocol/inspector node dist/index.js",
78
- "prepublishOnly": "npm run lint && npm run type-check && npm run build"
79
- },
80
- "dependencies": {
81
- "@modelcontextprotocol/sdk": "^1.29",
82
- "@mozilla/readability": "^0.6.0",
83
- "linkedom": "^0.18.12",
84
- "node-html-markdown": "^2.0.0",
85
- "undici": "^7.24.7",
86
- "zod": "^4.3.6"
87
- },
88
- "devDependencies": {
89
- "@eslint/js": "^10.0.1",
90
- "@trivago/prettier-plugin-sort-imports": "^6.0.2",
91
- "@types/node": "^24",
92
- "eslint": "^10.2.0",
93
- "eslint-config-prettier": "^10.1.8",
94
- "eslint-plugin-de-morgan": "^2.1.1",
95
- "eslint-plugin-depend": "^1.5.0",
96
- "eslint-plugin-sonarjs": "^4.0.2",
97
- "eslint-plugin-unicorn": "^64.0.0",
98
- "eslint-plugin-unused-imports": "^4.4.1",
99
- "knip": "^6.3.0",
100
- "prettier": "^3.8.1",
101
- "tsx": "^4.21.0",
102
- "typescript": "^6.0.2",
103
- "typescript-eslint": "^8.58.0"
104
- },
105
- "engines": {
106
- "node": ">=24"
107
- }
108
- }
1
+ {
2
+ "name": "@j0hanz/fetch-url-mcp",
3
+ "version": "2.0.1",
4
+ "mcpName": "io.github.j0hanz/fetch-url-mcp",
5
+ "description": "An MCP server that fetches web pages and converts them to clean, readable Markdown.",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "bin": {
10
+ "fetch-url-mcp": "dist/index.js"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ },
17
+ "./package.json": "./package.json"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "README.md"
22
+ ],
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/j0hanz/fetch-url-mcp.git"
26
+ },
27
+ "homepage": "https://github.com/j0hanz/fetch-url-mcp#readme",
28
+ "bugs": {
29
+ "url": "https://github.com/j0hanz/fetch-url-mcp/issues"
30
+ },
31
+ "author": "j0hanz",
32
+ "license": "MIT",
33
+ "keywords": [
34
+ "mcp",
35
+ "mcp-server",
36
+ "model-context-protocol",
37
+ "web-fetching",
38
+ "content-extraction",
39
+ "readability",
40
+ "markdown",
41
+ "html-to-markdown",
42
+ "web-scraper",
43
+ "llm-context",
44
+ "ai-tools",
45
+ "fetch-url-mcp",
46
+ "typescript",
47
+ "nodejs",
48
+ "stdio",
49
+ "streamable-http",
50
+ "structured-output",
51
+ "metadata-extraction",
52
+ "url-rewriting",
53
+ "worker-threads",
54
+ "content-cleanup"
55
+ ],
56
+ "scripts": {
57
+ "clean": "node scripts/tasks.mjs clean",
58
+ "build": "node scripts/tasks.mjs build",
59
+ "copy:assets": "node scripts/tasks.mjs copy:assets",
60
+ "prepare": "npm run build",
61
+ "dev": "tsc --watch --preserveWatchOutput",
62
+ "dev:run": "node --env-file=.env --watch dist/index.js",
63
+ "start": "node dist/index.js",
64
+ "format": "prettier --write .",
65
+ "type-check": "node scripts/tasks.mjs type-check",
66
+ "type-check:src": "node node_modules/typescript/bin/tsc -p tsconfig.json --noEmit",
67
+ "type-check:tests": "node node_modules/typescript/bin/tsc -p tsconfig.test.json --noEmit",
68
+ "type-check:diagnostics": "tsc --noEmit --extendedDiagnostics",
69
+ "type-check:trace": "node -e \"require('fs').rmSync('.ts-trace',{recursive:true,force:true})\" && tsc --noEmit --generateTrace .ts-trace",
70
+ "lint": "eslint .",
71
+ "lint:tests": "eslint __tests__",
72
+ "lint:fix": "eslint . --fix",
73
+ "test": "node scripts/tasks.mjs test",
74
+ "test:coverage": "node scripts/tasks.mjs test --coverage",
75
+ "knip": "knip",
76
+ "knip:fix": "knip --fix",
77
+ "inspector": "npm run build && npx -y @modelcontextprotocol/inspector node dist/index.js",
78
+ "prepublishOnly": "npm run lint && npm run type-check && npm run build"
79
+ },
80
+ "dependencies": {
81
+ "@cfworker/json-schema": "^4.1.1",
82
+ "@modelcontextprotocol/node": "^2.0.0-alpha.2",
83
+ "@modelcontextprotocol/server": "^2.0.0-alpha.2",
84
+ "@mozilla/readability": "^0.6.0",
85
+ "linkedom": "^0.18.12",
86
+ "node-html-markdown": "^2.0.0",
87
+ "undici": "^8.0.2",
88
+ "zod": "^4.3.6"
89
+ },
90
+ "devDependencies": {
91
+ "@eslint/js": "^10.0.1",
92
+ "@trivago/prettier-plugin-sort-imports": "^6.0.2",
93
+ "@types/node": "^24",
94
+ "eslint": "^10.2.0",
95
+ "eslint-config-prettier": "^10.1.8",
96
+ "eslint-plugin-de-morgan": "^2.1.1",
97
+ "eslint-plugin-depend": "^1.5.0",
98
+ "eslint-plugin-sonarjs": "^4.0.2",
99
+ "eslint-plugin-unicorn": "^64.0.0",
100
+ "eslint-plugin-unused-imports": "^4.4.1",
101
+ "knip": "^6.3.1",
102
+ "prettier": "^3.8.2",
103
+ "tsx": "^4.21.0",
104
+ "typescript": "^6.0.2",
105
+ "typescript-eslint": "^8.58.1"
106
+ },
107
+ "engines": {
108
+ "node": ">=24"
109
+ }
110
+ }