@j0hanz/fetch-url-mcp 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AGENTS.md +1 -0
- package/dist/assets/logo.svg +24836 -24836
- package/dist/cache.d.ts +1 -0
- package/dist/cache.js +13 -6
- package/dist/config.js +38 -38
- package/dist/fetch.js +31 -14
- package/dist/http-native.js +20 -15
- package/dist/index.js +0 -0
- package/dist/instructions.md +5 -2
- package/dist/mcp.js +60 -12
- package/dist/observability.js +3 -3
- package/dist/resources.js +88 -7
- package/dist/server.js +4 -1
- package/dist/session.js +12 -7
- package/dist/tools.js +74 -7
- package/dist/transform.js +37 -8
- package/dist/workers/transform-child.js +5 -4
- package/dist/workers/transform-worker.js +5 -4
- package/package.json +91 -91
package/dist/resources.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { ResourceTemplate, } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import { ErrorCode, McpError, } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
-
import { get as getCacheEntry, keys as listCacheKeys, parseCachedPayload, parseCacheKey, resolveCachedPayloadContent, } from './cache.js';
|
|
2
|
+
import { ErrorCode, McpError, SubscribeRequestSchema, UnsubscribeRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
import { get as getCacheEntry, keys as listCacheKeys, onCacheUpdate, parseCachedPayload, parseCacheKey, resolveCachedPayloadContent, } from './cache.js';
|
|
4
|
+
import { logWarn } from './observability.js';
|
|
5
|
+
import { isObject } from './type-guards.js';
|
|
4
6
|
const CACHE_RESOURCE_TEMPLATE_URI = 'internal://cache/{namespace}/{hash}';
|
|
5
7
|
const CACHE_RESOURCE_PREFIX = 'internal://cache/';
|
|
6
8
|
const CACHE_NAMESPACE_PATTERN = /^[a-z0-9_-]{1,64}$/i;
|
|
@@ -34,8 +36,8 @@ function firstVariableValue(value) {
|
|
|
34
36
|
return undefined;
|
|
35
37
|
}
|
|
36
38
|
function parseCacheResourceFromVariables(variables) {
|
|
37
|
-
const namespace = firstVariableValue(variables
|
|
38
|
-
const hash = firstVariableValue(variables
|
|
39
|
+
const namespace = firstVariableValue(variables['namespace']);
|
|
40
|
+
const hash = firstVariableValue(variables['hash']);
|
|
39
41
|
if (!namespace || !hash)
|
|
40
42
|
return null;
|
|
41
43
|
const decoded = {
|
|
@@ -86,7 +88,7 @@ function completeCacheNamespaces(value) {
|
|
|
86
88
|
}
|
|
87
89
|
function completeCacheHashes(value, context) {
|
|
88
90
|
const normalized = value.trim().toLowerCase();
|
|
89
|
-
const namespace = context?.arguments?.namespace?.trim();
|
|
91
|
+
const namespace = context?.arguments?.['namespace']?.trim();
|
|
90
92
|
const hashes = new Set();
|
|
91
93
|
for (const key of listCacheKeys()) {
|
|
92
94
|
const parsed = parseCacheKey(key);
|
|
@@ -106,7 +108,6 @@ function listCacheResources() {
|
|
|
106
108
|
const resources = listCacheKeys()
|
|
107
109
|
.map((key) => parseCacheKey(key))
|
|
108
110
|
.filter((parts) => Boolean(parts))
|
|
109
|
-
.slice(0, MAX_COMPLETION_VALUES)
|
|
110
111
|
.map((parts) => {
|
|
111
112
|
const cacheParts = {
|
|
112
113
|
namespace: parts.namespace,
|
|
@@ -126,6 +127,85 @@ function listCacheResources() {
|
|
|
126
127
|
});
|
|
127
128
|
return { resources };
|
|
128
129
|
}
|
|
130
|
+
function normalizeSubscriptionUri(uri) {
|
|
131
|
+
if (!URL.canParse(uri)) {
|
|
132
|
+
throw new McpError(ErrorCode.InvalidParams, 'Invalid resource URI');
|
|
133
|
+
}
|
|
134
|
+
const parsedUri = new URL(uri);
|
|
135
|
+
const cacheParts = parseCacheResourceFromUri(parsedUri);
|
|
136
|
+
if (cacheParts)
|
|
137
|
+
return toCacheResourceUri(cacheParts);
|
|
138
|
+
return parsedUri.href;
|
|
139
|
+
}
|
|
140
|
+
function registerCacheResourceNotifications(server) {
|
|
141
|
+
const subscribedResourceUris = new Set();
|
|
142
|
+
server.server.setRequestHandler(SubscribeRequestSchema, async (request) => {
|
|
143
|
+
subscribedResourceUris.add(normalizeSubscriptionUri(request.params.uri));
|
|
144
|
+
return Promise.resolve({});
|
|
145
|
+
});
|
|
146
|
+
server.server.setRequestHandler(UnsubscribeRequestSchema, async (request) => {
|
|
147
|
+
subscribedResourceUris.delete(normalizeSubscriptionUri(request.params.uri));
|
|
148
|
+
return Promise.resolve({});
|
|
149
|
+
});
|
|
150
|
+
const unsubscribe = onCacheUpdate((event) => {
|
|
151
|
+
const changedUri = toCacheResourceUri({
|
|
152
|
+
namespace: event.namespace,
|
|
153
|
+
hash: event.urlHash,
|
|
154
|
+
});
|
|
155
|
+
if (server.isConnected() && subscribedResourceUris.has(changedUri)) {
|
|
156
|
+
void server.server
|
|
157
|
+
.sendResourceUpdated({ uri: changedUri })
|
|
158
|
+
.catch((error) => {
|
|
159
|
+
logWarn('Failed to send resource updated notification', {
|
|
160
|
+
uri: changedUri,
|
|
161
|
+
error,
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
if (!event.listChanged)
|
|
166
|
+
return;
|
|
167
|
+
if (!server.isConnected())
|
|
168
|
+
return;
|
|
169
|
+
try {
|
|
170
|
+
server.sendResourceListChanged();
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
logWarn('Failed to send resources list changed notification', { error });
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
let cleanedUp = false;
|
|
177
|
+
const cleanup = () => {
|
|
178
|
+
if (cleanedUp)
|
|
179
|
+
return;
|
|
180
|
+
cleanedUp = true;
|
|
181
|
+
unsubscribe();
|
|
182
|
+
};
|
|
183
|
+
const originalOnClose = server.server.onclose;
|
|
184
|
+
server.server.onclose = () => {
|
|
185
|
+
cleanup();
|
|
186
|
+
originalOnClose?.();
|
|
187
|
+
};
|
|
188
|
+
const originalClose = server.close.bind(server);
|
|
189
|
+
server.close = async () => {
|
|
190
|
+
cleanup();
|
|
191
|
+
await originalClose();
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function normalizeTemplateVariables(variables) {
|
|
195
|
+
if (!isObject(variables))
|
|
196
|
+
return {};
|
|
197
|
+
const normalized = {};
|
|
198
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
199
|
+
if (typeof value === 'string' || value === undefined) {
|
|
200
|
+
normalized[key] = value;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (Array.isArray(value)) {
|
|
204
|
+
normalized[key] = value.filter((item) => typeof item === 'string');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return normalized;
|
|
208
|
+
}
|
|
129
209
|
function resolveCacheResourceParts(uri, variables) {
|
|
130
210
|
const fromVariables = parseCacheResourceFromVariables(variables);
|
|
131
211
|
if (fromVariables)
|
|
@@ -212,5 +292,6 @@ export function registerCacheResourceTemplate(server, iconInfo) {
|
|
|
212
292
|
],
|
|
213
293
|
}
|
|
214
294
|
: {}),
|
|
215
|
-
}, (uri, variables) => readCacheResource(uri, variables));
|
|
295
|
+
}, (uri, variables) => readCacheResource(uri, normalizeTemplateVariables(variables)));
|
|
296
|
+
registerCacheResourceNotifications(server);
|
|
216
297
|
}
|
package/dist/server.js
CHANGED
package/dist/session.js
CHANGED
|
@@ -56,12 +56,15 @@ class SessionCleanupLoop {
|
|
|
56
56
|
for await (const getNow of ticks) {
|
|
57
57
|
const now = getNow();
|
|
58
58
|
const evicted = this.store.evictExpired();
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
session.server
|
|
64
|
-
|
|
59
|
+
const closeBatchSize = 10;
|
|
60
|
+
for (let i = 0; i < evicted.length; i += closeBatchSize) {
|
|
61
|
+
const batch = evicted.slice(i, i + closeBatchSize);
|
|
62
|
+
await Promise.allSettled(batch.map(async (session) => {
|
|
63
|
+
unregisterMcpSessionServerByServer(session.server);
|
|
64
|
+
const results = await Promise.allSettled([
|
|
65
|
+
session.transport.close(),
|
|
66
|
+
session.server.close(),
|
|
67
|
+
]);
|
|
65
68
|
const [transportResult, serverResult] = results;
|
|
66
69
|
if (transportResult.status === 'rejected') {
|
|
67
70
|
logWarn('Failed to close expired session transport', {
|
|
@@ -73,7 +76,9 @@ class SessionCleanupLoop {
|
|
|
73
76
|
error: formatError(serverResult.reason),
|
|
74
77
|
});
|
|
75
78
|
}
|
|
76
|
-
});
|
|
79
|
+
}));
|
|
80
|
+
if (signal.aborted)
|
|
81
|
+
return;
|
|
77
82
|
}
|
|
78
83
|
if (evicted.length > 0) {
|
|
79
84
|
logInfo('Expired sessions evicted', {
|
package/dist/tools.js
CHANGED
|
@@ -53,6 +53,11 @@ const fetchUrlOutputSchema = z.strictObject({
|
|
|
53
53
|
.max(config.constants.maxUrlLength)
|
|
54
54
|
.optional()
|
|
55
55
|
.describe('The final response URL after redirects'),
|
|
56
|
+
cacheResourceUri: z
|
|
57
|
+
.string()
|
|
58
|
+
.max(config.constants.maxUrlLength)
|
|
59
|
+
.optional()
|
|
60
|
+
.describe('Internal cache resource URI for retrieving full markdown via resources/read'),
|
|
56
61
|
title: z.string().max(512).optional().describe('Page title'),
|
|
57
62
|
metadata: z
|
|
58
63
|
.strictObject({
|
|
@@ -390,7 +395,7 @@ function applyInlineContentLimit(content, inlineLimitOverride) {
|
|
|
390
395
|
return inlineLimiter.apply(content, inlineLimitOverride);
|
|
391
396
|
}
|
|
392
397
|
/* -------------------------------------------------------------------------------------------------
|
|
393
|
-
* Tool response blocks (text
|
|
398
|
+
* Tool response blocks (text + optional embedded resource)
|
|
394
399
|
* ------------------------------------------------------------------------------------------------- */
|
|
395
400
|
function buildTextBlock(structuredContent) {
|
|
396
401
|
return {
|
|
@@ -398,8 +403,46 @@ function buildTextBlock(structuredContent) {
|
|
|
398
403
|
text: JSON.stringify(structuredContent),
|
|
399
404
|
};
|
|
400
405
|
}
|
|
401
|
-
function
|
|
402
|
-
|
|
406
|
+
function buildEmbeddedResource(content, url, title) {
|
|
407
|
+
if (!content)
|
|
408
|
+
return null;
|
|
409
|
+
const filename = cache.generateSafeFilename(url, title, undefined, '.md');
|
|
410
|
+
const uri = new URL(filename, 'file:///').href;
|
|
411
|
+
const resource = {
|
|
412
|
+
uri,
|
|
413
|
+
mimeType: 'text/markdown',
|
|
414
|
+
text: content,
|
|
415
|
+
};
|
|
416
|
+
return {
|
|
417
|
+
type: 'resource',
|
|
418
|
+
resource,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
function buildCacheResourceLink(cacheResourceUri, contentSize, fetchedAt) {
|
|
422
|
+
return {
|
|
423
|
+
type: 'resource_link',
|
|
424
|
+
uri: cacheResourceUri,
|
|
425
|
+
name: 'cached-markdown',
|
|
426
|
+
title: 'Cached Fetch Output',
|
|
427
|
+
description: 'Read full markdown via resources/read.',
|
|
428
|
+
mimeType: 'text/markdown',
|
|
429
|
+
...(contentSize > 0 ? { size: contentSize } : {}),
|
|
430
|
+
annotations: {
|
|
431
|
+
audience: ['assistant'],
|
|
432
|
+
priority: 0.8,
|
|
433
|
+
lastModified: fetchedAt,
|
|
434
|
+
},
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function buildToolContentBlocks(structuredContent, resourceLink, embeddedResource) {
|
|
438
|
+
const blocks = [buildTextBlock(structuredContent)];
|
|
439
|
+
if (resourceLink) {
|
|
440
|
+
blocks.push(resourceLink);
|
|
441
|
+
}
|
|
442
|
+
if (embeddedResource) {
|
|
443
|
+
blocks.push(embeddedResource);
|
|
444
|
+
}
|
|
445
|
+
return blocks;
|
|
403
446
|
}
|
|
404
447
|
function resolveNormalizedUrl(url) {
|
|
405
448
|
const { normalizedUrl: validatedUrl } = normalizeUrl(url);
|
|
@@ -415,7 +458,7 @@ function logRawUrlTransformation(resolvedUrl) {
|
|
|
415
458
|
}
|
|
416
459
|
function extractTitle(value) {
|
|
417
460
|
const record = asRecord(value);
|
|
418
|
-
const title = record ? record
|
|
461
|
+
const title = record ? record['title'] : undefined;
|
|
419
462
|
return typeof title === 'string' ? title : undefined;
|
|
420
463
|
}
|
|
421
464
|
function logCacheMiss(reason, cacheNamespace, normalizedUrl, error) {
|
|
@@ -663,6 +706,7 @@ function serializeMarkdownResult(result) {
|
|
|
663
706
|
* fetch-url tool implementation
|
|
664
707
|
* ------------------------------------------------------------------------------------------------- */
|
|
665
708
|
function buildStructuredContent(pipeline, inlineResult, inputUrl) {
|
|
709
|
+
const cacheResourceUri = resolveCacheResourceUri(pipeline.cacheKey);
|
|
666
710
|
const truncated = inlineResult.truncated ?? pipeline.data.truncated;
|
|
667
711
|
let markdown = inlineResult.content;
|
|
668
712
|
if (pipeline.data.truncated &&
|
|
@@ -675,6 +719,7 @@ function buildStructuredContent(pipeline, inlineResult, inputUrl) {
|
|
|
675
719
|
url: pipeline.originalUrl ?? pipeline.url,
|
|
676
720
|
resolvedUrl: pipeline.url,
|
|
677
721
|
...(pipeline.finalUrl ? { finalUrl: pipeline.finalUrl } : {}),
|
|
722
|
+
...(cacheResourceUri ? { cacheResourceUri } : {}),
|
|
678
723
|
inputUrl,
|
|
679
724
|
title: pipeline.data.title,
|
|
680
725
|
...(metadata ? { metadata } : {}),
|
|
@@ -685,12 +730,34 @@ function buildStructuredContent(pipeline, inlineResult, inputUrl) {
|
|
|
685
730
|
...(truncated ? { truncated: true } : {}),
|
|
686
731
|
};
|
|
687
732
|
}
|
|
688
|
-
function
|
|
689
|
-
|
|
733
|
+
function resolveCacheResourceUri(cacheKey) {
|
|
734
|
+
if (!cacheKey)
|
|
735
|
+
return undefined;
|
|
736
|
+
if (!cache.isEnabled())
|
|
737
|
+
return undefined;
|
|
738
|
+
if (!cache.get(cacheKey))
|
|
739
|
+
return undefined;
|
|
740
|
+
const parsed = cache.parseCacheKey(cacheKey);
|
|
741
|
+
if (!parsed)
|
|
742
|
+
return undefined;
|
|
743
|
+
return `internal://cache/${encodeURIComponent(parsed.namespace)}/${encodeURIComponent(parsed.urlHash)}`;
|
|
744
|
+
}
|
|
745
|
+
function buildFetchUrlContentBlocks(structuredContent, pipeline, inlineResult) {
|
|
746
|
+
const cacheResourceUri = readString(structuredContent, 'cacheResourceUri');
|
|
747
|
+
const contentToEmbed = config.runtime.httpMode
|
|
748
|
+
? inlineResult.content
|
|
749
|
+
: pipeline.data.content;
|
|
750
|
+
const resourceLink = cacheResourceUri
|
|
751
|
+
? buildCacheResourceLink(cacheResourceUri, inlineResult.contentSize, pipeline.fetchedAt)
|
|
752
|
+
: null;
|
|
753
|
+
const embedded = contentToEmbed && pipeline.url
|
|
754
|
+
? buildEmbeddedResource(contentToEmbed, pipeline.url, pipeline.data.title)
|
|
755
|
+
: null;
|
|
756
|
+
return buildToolContentBlocks(structuredContent, resourceLink, embedded);
|
|
690
757
|
}
|
|
691
758
|
function buildResponse(pipeline, inlineResult, inputUrl) {
|
|
692
759
|
const structuredContent = buildStructuredContent(pipeline, inlineResult, inputUrl);
|
|
693
|
-
const content = buildFetchUrlContentBlocks(structuredContent);
|
|
760
|
+
const content = buildFetchUrlContentBlocks(structuredContent, pipeline, inlineResult);
|
|
694
761
|
// Runtime validation guard: verify output matches schema
|
|
695
762
|
const validation = fetchUrlOutputSchema.safeParse(structuredContent);
|
|
696
763
|
if (!validation.success) {
|
package/dist/transform.js
CHANGED
|
@@ -44,7 +44,7 @@ function getTagName(node) {
|
|
|
44
44
|
}
|
|
45
45
|
function getAbortReason(signal) {
|
|
46
46
|
const record = isObject(signal) ? signal : null;
|
|
47
|
-
return record && 'reason' in record ? record
|
|
47
|
+
return record && 'reason' in record ? record['reason'] : undefined;
|
|
48
48
|
}
|
|
49
49
|
function isTimeoutAbortReason(reason) {
|
|
50
50
|
return reason instanceof Error && reason.name === 'TimeoutError';
|
|
@@ -424,6 +424,35 @@ function isReadabilityCompatible(doc) {
|
|
|
424
424
|
'function' &&
|
|
425
425
|
typeof record.querySelector === 'function');
|
|
426
426
|
}
|
|
427
|
+
function resolveCollapsedTextLengthUpTo(text, max) {
|
|
428
|
+
if (max <= 0)
|
|
429
|
+
return 0;
|
|
430
|
+
let length = 0;
|
|
431
|
+
let seenNonWhitespace = false;
|
|
432
|
+
let pendingSpace = false;
|
|
433
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
434
|
+
const code = text.charCodeAt(i);
|
|
435
|
+
const isWhitespace = code <= 0x20;
|
|
436
|
+
if (isWhitespace) {
|
|
437
|
+
if (seenNonWhitespace)
|
|
438
|
+
pendingSpace = true;
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (!seenNonWhitespace) {
|
|
442
|
+
seenNonWhitespace = true;
|
|
443
|
+
}
|
|
444
|
+
else if (pendingSpace) {
|
|
445
|
+
length += 1;
|
|
446
|
+
pendingSpace = false;
|
|
447
|
+
if (length >= max)
|
|
448
|
+
return length;
|
|
449
|
+
}
|
|
450
|
+
length += 1;
|
|
451
|
+
if (length >= max)
|
|
452
|
+
return length;
|
|
453
|
+
}
|
|
454
|
+
return length;
|
|
455
|
+
}
|
|
427
456
|
function extractArticle(document, url, signal) {
|
|
428
457
|
if (!isReadabilityCompatible(document)) {
|
|
429
458
|
logWarn('Document not compatible with Readability');
|
|
@@ -436,7 +465,7 @@ function extractArticle(document, url, signal) {
|
|
|
436
465
|
const rawText = doc.querySelector('body')?.textContent ??
|
|
437
466
|
doc.documentElement.textContent ??
|
|
438
467
|
'';
|
|
439
|
-
const textLength = rawText
|
|
468
|
+
const textLength = resolveCollapsedTextLengthUpTo(rawText, 401);
|
|
440
469
|
if (textLength < 100) {
|
|
441
470
|
logWarn('Very minimal server-rendered content detected (< 100 chars). ' +
|
|
442
471
|
'This might be a client-side rendered (SPA) application. ' +
|
|
@@ -1610,13 +1639,13 @@ function isWorkerErrorPayload(value) {
|
|
|
1610
1639
|
function isWorkerResponse(raw) {
|
|
1611
1640
|
if (!isObject(raw))
|
|
1612
1641
|
return false;
|
|
1613
|
-
if (typeof raw
|
|
1642
|
+
if (typeof raw['id'] !== 'string')
|
|
1614
1643
|
return false;
|
|
1615
|
-
if (raw
|
|
1616
|
-
return isWorkerResultPayload(raw
|
|
1644
|
+
if (raw['type'] === 'result') {
|
|
1645
|
+
return isWorkerResultPayload(raw['result']);
|
|
1617
1646
|
}
|
|
1618
|
-
if (raw
|
|
1619
|
-
return isWorkerErrorPayload(raw
|
|
1647
|
+
if (raw['type'] === 'error') {
|
|
1648
|
+
return isWorkerErrorPayload(raw['error']);
|
|
1620
1649
|
}
|
|
1621
1650
|
return false;
|
|
1622
1651
|
}
|
|
@@ -2287,7 +2316,7 @@ async function transformWithWorkerPool(htmlOrBuffer, url, options) {
|
|
|
2287
2316
|
});
|
|
2288
2317
|
}
|
|
2289
2318
|
function resolveWorkerFallback(error, htmlOrBuffer, url, options) {
|
|
2290
|
-
const isQueueFull = error instanceof FetchError && error.details
|
|
2319
|
+
const isQueueFull = error instanceof FetchError && error.details['reason'] === 'queue_full';
|
|
2291
2320
|
if (isQueueFull) {
|
|
2292
2321
|
logWarn('Transform worker queue full; falling back to in-process', {
|
|
2293
2322
|
url: redactUrl(url),
|
|
@@ -122,15 +122,16 @@ process.on('message', (raw) => {
|
|
|
122
122
|
if (!raw || typeof raw !== 'object')
|
|
123
123
|
return;
|
|
124
124
|
const msg = raw;
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
const { type, id } = msg;
|
|
126
|
+
if (type === 'cancel') {
|
|
127
|
+
if (typeof id !== 'string')
|
|
127
128
|
return;
|
|
128
|
-
const controller = controllersById.get(
|
|
129
|
+
const controller = controllersById.get(id);
|
|
129
130
|
if (controller)
|
|
130
131
|
controller.abort(new Error('Canceled'));
|
|
131
132
|
return;
|
|
132
133
|
}
|
|
133
|
-
if (
|
|
134
|
+
if (type === 'transform') {
|
|
134
135
|
handleTransform(msg);
|
|
135
136
|
}
|
|
136
137
|
});
|
|
@@ -114,15 +114,16 @@ port.on('message', (raw) => {
|
|
|
114
114
|
if (!raw || typeof raw !== 'object')
|
|
115
115
|
return;
|
|
116
116
|
const msg = raw;
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
const { type, id } = msg;
|
|
118
|
+
if (type === 'cancel') {
|
|
119
|
+
if (typeof id !== 'string')
|
|
119
120
|
return;
|
|
120
|
-
const controller = controllersById.get(
|
|
121
|
+
const controller = controllersById.get(id);
|
|
121
122
|
if (controller)
|
|
122
123
|
controller.abort(new Error('Canceled'));
|
|
123
124
|
return;
|
|
124
125
|
}
|
|
125
|
-
if (
|
|
126
|
+
if (type === 'transform') {
|
|
126
127
|
handleTransform(msg);
|
|
127
128
|
}
|
|
128
129
|
});
|
package/package.json
CHANGED
|
@@ -1,91 +1,91 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@j0hanz/fetch-url-mcp",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"mcpName": "io.github.j0hanz/fetch-url-mcp",
|
|
5
|
-
"description": "Intelligent web content fetcher MCP server that converts HTML to clean, AI-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
|
-
"web-fetching",
|
|
37
|
-
"content-extraction",
|
|
38
|
-
"readability",
|
|
39
|
-
"markdown",
|
|
40
|
-
"ai-tools",
|
|
41
|
-
"model-context-protocol",
|
|
42
|
-
"fetch-url-mcp"
|
|
43
|
-
],
|
|
44
|
-
"scripts": {
|
|
45
|
-
"clean": "node scripts/tasks.mjs clean",
|
|
46
|
-
"validate:instructions": "node scripts/tasks.mjs validate:instructions",
|
|
47
|
-
"build": "node scripts/tasks.mjs build",
|
|
48
|
-
"copy:assets": "node scripts/tasks.mjs copy:assets",
|
|
49
|
-
"prepare": "npm run build",
|
|
50
|
-
"dev": "tsc --watch --preserveWatchOutput",
|
|
51
|
-
"dev:run": "node --env-file=.env --watch dist/index.js",
|
|
52
|
-
"start": "node dist/index.js",
|
|
53
|
-
"format": "prettier --write .",
|
|
54
|
-
"type-check": "node scripts/tasks.mjs type-check",
|
|
55
|
-
"type-check:diagnostics": "tsc --noEmit --extendedDiagnostics",
|
|
56
|
-
"type-check:trace": "node -e \"require('fs').rmSync('.ts-trace',{recursive:true,force:true})\" && tsc --noEmit --generateTrace .ts-trace",
|
|
57
|
-
"lint": "eslint .",
|
|
58
|
-
"lint:fix": "eslint . --fix",
|
|
59
|
-
"test": "node scripts/tasks.mjs test",
|
|
60
|
-
"test:coverage": "node scripts/tasks.mjs test --coverage",
|
|
61
|
-
"knip": "knip",
|
|
62
|
-
"knip:fix": "knip --fix",
|
|
63
|
-
"inspector": "npm run build && npx -y @modelcontextprotocol/inspector node dist/index.js --stdio",
|
|
64
|
-
"prepublishOnly": "npm run lint && npm run type-check && npm run build"
|
|
65
|
-
},
|
|
66
|
-
"dependencies": {
|
|
67
|
-
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
68
|
-
"@mozilla/readability": "^0.6.0",
|
|
69
|
-
"linkedom": "^0.18.12",
|
|
70
|
-
"node-html-markdown": "^2.0.0",
|
|
71
|
-
"zod": "^4.3.6"
|
|
72
|
-
},
|
|
73
|
-
"devDependencies": {
|
|
74
|
-
"@eslint/js": "^9.39.2",
|
|
75
|
-
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
|
76
|
-
"@types/node": "^24",
|
|
77
|
-
"eslint": "^9.23.2",
|
|
78
|
-
"eslint-config-prettier": "^10.1.8",
|
|
79
|
-
"eslint-plugin-de-morgan": "^2.0.0",
|
|
80
|
-
"eslint-plugin-depend": "^1.4.0",
|
|
81
|
-
"eslint-plugin-sonarjs": "^3.0.6",
|
|
82
|
-
"eslint-plugin-unused-imports": "^4.4.1",
|
|
83
|
-
"knip": "^5.83.1",
|
|
84
|
-
"prettier": "^3.8.1",
|
|
85
|
-
"typescript": "^5.9.3",
|
|
86
|
-
"typescript-eslint": "^8.55.0"
|
|
87
|
-
},
|
|
88
|
-
"engines": {
|
|
89
|
-
"node": ">=24"
|
|
90
|
-
}
|
|
91
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@j0hanz/fetch-url-mcp",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"mcpName": "io.github.j0hanz/fetch-url-mcp",
|
|
5
|
+
"description": "Intelligent web content fetcher MCP server that converts HTML to clean, AI-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
|
+
"web-fetching",
|
|
37
|
+
"content-extraction",
|
|
38
|
+
"readability",
|
|
39
|
+
"markdown",
|
|
40
|
+
"ai-tools",
|
|
41
|
+
"model-context-protocol",
|
|
42
|
+
"fetch-url-mcp"
|
|
43
|
+
],
|
|
44
|
+
"scripts": {
|
|
45
|
+
"clean": "node scripts/tasks.mjs clean",
|
|
46
|
+
"validate:instructions": "node scripts/tasks.mjs validate:instructions",
|
|
47
|
+
"build": "node scripts/tasks.mjs build",
|
|
48
|
+
"copy:assets": "node scripts/tasks.mjs copy:assets",
|
|
49
|
+
"prepare": "npm run build",
|
|
50
|
+
"dev": "tsc --watch --preserveWatchOutput",
|
|
51
|
+
"dev:run": "node --env-file=.env --watch dist/index.js",
|
|
52
|
+
"start": "node dist/index.js",
|
|
53
|
+
"format": "prettier --write .",
|
|
54
|
+
"type-check": "node scripts/tasks.mjs type-check",
|
|
55
|
+
"type-check:diagnostics": "tsc --noEmit --extendedDiagnostics",
|
|
56
|
+
"type-check:trace": "node -e \"require('fs').rmSync('.ts-trace',{recursive:true,force:true})\" && tsc --noEmit --generateTrace .ts-trace",
|
|
57
|
+
"lint": "eslint .",
|
|
58
|
+
"lint:fix": "eslint . --fix",
|
|
59
|
+
"test": "node scripts/tasks.mjs test",
|
|
60
|
+
"test:coverage": "node scripts/tasks.mjs test --coverage",
|
|
61
|
+
"knip": "knip",
|
|
62
|
+
"knip:fix": "knip --fix",
|
|
63
|
+
"inspector": "npm run build && npx -y @modelcontextprotocol/inspector node dist/index.js --stdio",
|
|
64
|
+
"prepublishOnly": "npm run lint && npm run type-check && npm run build"
|
|
65
|
+
},
|
|
66
|
+
"dependencies": {
|
|
67
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
68
|
+
"@mozilla/readability": "^0.6.0",
|
|
69
|
+
"linkedom": "^0.18.12",
|
|
70
|
+
"node-html-markdown": "^2.0.0",
|
|
71
|
+
"zod": "^4.3.6"
|
|
72
|
+
},
|
|
73
|
+
"devDependencies": {
|
|
74
|
+
"@eslint/js": "^9.39.2",
|
|
75
|
+
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
|
76
|
+
"@types/node": "^24",
|
|
77
|
+
"eslint": "^9.23.2",
|
|
78
|
+
"eslint-config-prettier": "^10.1.8",
|
|
79
|
+
"eslint-plugin-de-morgan": "^2.0.0",
|
|
80
|
+
"eslint-plugin-depend": "^1.4.0",
|
|
81
|
+
"eslint-plugin-sonarjs": "^3.0.6",
|
|
82
|
+
"eslint-plugin-unused-imports": "^4.4.1",
|
|
83
|
+
"knip": "^5.83.1",
|
|
84
|
+
"prettier": "^3.8.1",
|
|
85
|
+
"typescript": "^5.9.3",
|
|
86
|
+
"typescript-eslint": "^8.55.0"
|
|
87
|
+
},
|
|
88
|
+
"engines": {
|
|
89
|
+
"node": ">=24"
|
|
90
|
+
}
|
|
91
|
+
}
|