@j0hanz/fetch-url-mcp 1.11.7 → 1.12.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/LICENSE +21 -0
- package/README.md +177 -99
- package/dist/http/health.d.ts.map +1 -1
- package/dist/http/health.js +0 -2
- package/dist/http/native.d.ts.map +1 -1
- package/dist/http/native.js +0 -25
- package/dist/lib/config.d.ts +0 -7
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +0 -9
- package/dist/lib/core.d.ts +0 -3
- package/dist/lib/core.d.ts.map +1 -1
- package/dist/lib/core.js +0 -7
- package/dist/lib/fetch-pipeline.d.ts +1 -14
- package/dist/lib/fetch-pipeline.d.ts.map +1 -1
- package/dist/lib/fetch-pipeline.js +4 -147
- package/dist/lib/http.d.ts +0 -3
- package/dist/lib/http.d.ts.map +1 -1
- package/dist/lib/http.js +2 -105
- package/dist/lib/mcp-interop.js +2 -2
- package/dist/lib/utils.d.ts +0 -2
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +4 -39
- package/dist/resources/index.d.ts +1 -23
- package/dist/resources/index.d.ts.map +1 -1
- package/dist/resources/index.js +3 -294
- package/dist/schemas.d.ts +0 -14
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +1 -77
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -2
- package/dist/tools/fetch-url.d.ts +0 -2
- package/dist/tools/fetch-url.d.ts.map +1 -1
- package/dist/tools/fetch-url.js +12 -43
- package/dist/transform/shared.js +4 -4
- package/dist/transform/transform.d.ts +1 -1
- package/dist/transform/transform.d.ts.map +1 -1
- package/dist/transform/transform.js +10 -10
- package/dist/transform/types.d.ts +2 -2
- package/dist/transform/types.d.ts.map +1 -1
- package/dist/transform/worker-pool.d.ts +3 -3
- package/dist/transform/worker-pool.d.ts.map +1 -1
- package/dist/transform/worker-pool.js +2 -2
- package/package.json +13 -4
- package/dist/lib/cache.d.ts +0 -48
- package/dist/lib/cache.d.ts.map +0 -1
- package/dist/lib/cache.js +0 -264
package/dist/resources/index.js
CHANGED
|
@@ -1,271 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ErrorCode, McpError, SubscribeRequestSchema, UnsubscribeRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
-
import { get as getCacheEntry, getEntryMeta, keys as listCacheKeys, onCacheUpdate, parseCacheKey, toCacheScopeId, } from '../lib/cache.js';
|
|
4
|
-
import { config, logWarn, resolveMcpSessionIdByServer } from '../lib/core.js';
|
|
5
|
-
import { registerServerLifecycleCleanup } from '../lib/mcp-interop.js';
|
|
1
|
+
import { config } from '../lib/core.js';
|
|
6
2
|
import { buildOptionalIcons } from '../lib/utils.js';
|
|
7
|
-
import { isObject } from '../lib/utils.js';
|
|
8
|
-
import { parseCachedPayload, resolveCachedPayloadContent } from '../schemas.js';
|
|
9
3
|
import { FETCH_URL_TOOL_NAME } from '../tools/fetch-url.js';
|
|
10
|
-
const RESOURCE_NOT_FOUND_ERROR_CODE = -32002;
|
|
11
|
-
const CACHE_RESOURCE_TEMPLATE_URI = 'internal://cache/{namespace}/{hash}';
|
|
12
|
-
const CACHE_RESOURCE_PREFIX = 'internal://cache/';
|
|
13
|
-
const CACHE_NAMESPACE_PATTERN = /^[a-z0-9_-]{1,64}$/i;
|
|
14
|
-
const CACHE_HASH_PATTERN = /^[a-f0-9.]{8,64}$/i;
|
|
15
|
-
const MAX_COMPLETION_VALUES = 100;
|
|
16
|
-
function normalizeCompletionPrefix(value) {
|
|
17
|
-
return value.trim().toLowerCase();
|
|
18
|
-
}
|
|
19
|
-
function sortAndLimitValues(values) {
|
|
20
|
-
return [...values]
|
|
21
|
-
.sort((left, right) => left.localeCompare(right))
|
|
22
|
-
.slice(0, MAX_COMPLETION_VALUES);
|
|
23
|
-
}
|
|
24
|
-
function isValidCacheResourceParts(parts) {
|
|
25
|
-
return (CACHE_NAMESPACE_PATTERN.test(parts.namespace) &&
|
|
26
|
-
CACHE_HASH_PATTERN.test(parts.hash));
|
|
27
|
-
}
|
|
28
|
-
function decodeSegment(value) {
|
|
29
|
-
try {
|
|
30
|
-
return decodeURIComponent(value);
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
return value;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
function trimToValue(value) {
|
|
37
|
-
const trimmed = value.trim();
|
|
38
|
-
return trimmed.length > 0 ? trimmed : undefined;
|
|
39
|
-
}
|
|
40
|
-
function firstVariableValue(value) {
|
|
41
|
-
if (typeof value === 'string') {
|
|
42
|
-
return trimToValue(value);
|
|
43
|
-
}
|
|
44
|
-
if (Array.isArray(value)) {
|
|
45
|
-
const first = value[0];
|
|
46
|
-
if (typeof first !== 'string')
|
|
47
|
-
return undefined;
|
|
48
|
-
return trimToValue(first);
|
|
49
|
-
}
|
|
50
|
-
return undefined;
|
|
51
|
-
}
|
|
52
|
-
function validateCacheResourceParts(namespace, hash) {
|
|
53
|
-
const decoded = {
|
|
54
|
-
namespace: decodeSegment(namespace),
|
|
55
|
-
hash: decodeSegment(hash),
|
|
56
|
-
};
|
|
57
|
-
return isValidCacheResourceParts(decoded) ? decoded : null;
|
|
58
|
-
}
|
|
59
|
-
function parseCacheResourceFromVariables(variables) {
|
|
60
|
-
const namespace = firstVariableValue(variables['namespace']);
|
|
61
|
-
const hash = firstVariableValue(variables['hash']);
|
|
62
|
-
if (!namespace || !hash)
|
|
63
|
-
return null;
|
|
64
|
-
return validateCacheResourceParts(namespace, hash);
|
|
65
|
-
}
|
|
66
|
-
function parseCacheResourceFromUri(uri) {
|
|
67
|
-
if (!uri.href.startsWith(CACHE_RESOURCE_PREFIX))
|
|
68
|
-
return null;
|
|
69
|
-
const rawPath = uri.pathname.startsWith('/')
|
|
70
|
-
? uri.pathname.slice(1)
|
|
71
|
-
: uri.pathname;
|
|
72
|
-
const segments = rawPath.split('/');
|
|
73
|
-
if (segments.length !== 2)
|
|
74
|
-
return null;
|
|
75
|
-
const namespace = segments[0];
|
|
76
|
-
const hash = segments[1];
|
|
77
|
-
if (!namespace || !hash)
|
|
78
|
-
return null;
|
|
79
|
-
return validateCacheResourceParts(namespace, hash);
|
|
80
|
-
}
|
|
81
|
-
function toCacheResourceUri(parts) {
|
|
82
|
-
const namespace = encodeURIComponent(parts.namespace);
|
|
83
|
-
const hash = encodeURIComponent(parts.hash);
|
|
84
|
-
return `${CACHE_RESOURCE_PREFIX}${namespace}/${hash}`;
|
|
85
|
-
}
|
|
86
|
-
export function isCacheEntryVisibleToScope(scopeId, meta) {
|
|
87
|
-
return meta.scopeIds.includes(scopeId);
|
|
88
|
-
}
|
|
89
|
-
function getVisibleCacheEntries(scopeId) {
|
|
90
|
-
return listCacheKeys()
|
|
91
|
-
.map((key) => parseCacheKey(key))
|
|
92
|
-
.filter((parts) => parts !== null)
|
|
93
|
-
.map((parts) => ({ namespace: parts.namespace, hash: parts.urlHash }))
|
|
94
|
-
.filter((parts) => {
|
|
95
|
-
const meta = getEntryMeta(`${parts.namespace}:${parts.hash}`);
|
|
96
|
-
return meta ? isCacheEntryVisibleToScope(scopeId, meta) : false;
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
function completeCacheNamespaces(value, scopeId) {
|
|
100
|
-
const normalized = normalizeCompletionPrefix(value);
|
|
101
|
-
const namespaces = [
|
|
102
|
-
...new Set(getVisibleCacheEntries(scopeId).map((entry) => entry.namespace)),
|
|
103
|
-
].filter((ns) => ns.toLowerCase().startsWith(normalized));
|
|
104
|
-
return sortAndLimitValues(namespaces);
|
|
105
|
-
}
|
|
106
|
-
function completeCacheHashes(value, scopeId, context) {
|
|
107
|
-
const normalized = normalizeCompletionPrefix(value);
|
|
108
|
-
const namespace = context?.arguments?.['namespace']?.trim();
|
|
109
|
-
const hashes = getVisibleCacheEntries(scopeId)
|
|
110
|
-
.filter((entry) => namespace === undefined || entry.namespace === namespace)
|
|
111
|
-
.map((entry) => entry.hash)
|
|
112
|
-
.filter((h) => h.toLowerCase().startsWith(normalized));
|
|
113
|
-
return sortAndLimitValues(hashes);
|
|
114
|
-
}
|
|
115
|
-
function getServerCacheScopeId(server) {
|
|
116
|
-
return toCacheScopeId(resolveMcpSessionIdByServer(server));
|
|
117
|
-
}
|
|
118
|
-
export function listCacheResourcesForScope(scopeId) {
|
|
119
|
-
const resources = getVisibleCacheEntries(scopeId)
|
|
120
|
-
.map((parts) => {
|
|
121
|
-
const cacheParts = {
|
|
122
|
-
namespace: parts.namespace,
|
|
123
|
-
hash: parts.hash,
|
|
124
|
-
};
|
|
125
|
-
const cacheKey = `${parts.namespace}:${parts.hash}`;
|
|
126
|
-
const meta = getEntryMeta(cacheKey);
|
|
127
|
-
if (!meta)
|
|
128
|
-
return null; // expired between keys() and meta read — skip
|
|
129
|
-
return {
|
|
130
|
-
uri: toCacheResourceUri(cacheParts),
|
|
131
|
-
name: `${parts.namespace}:${parts.hash}`,
|
|
132
|
-
title: meta.title ?? 'Cached Markdown',
|
|
133
|
-
description: 'Cached markdown output generated by fetch-url',
|
|
134
|
-
mimeType: 'text/markdown',
|
|
135
|
-
annotations: {
|
|
136
|
-
audience: ['assistant'],
|
|
137
|
-
priority: 0.6,
|
|
138
|
-
...(meta.fetchedAt ? { lastModified: meta.fetchedAt } : {}),
|
|
139
|
-
},
|
|
140
|
-
};
|
|
141
|
-
})
|
|
142
|
-
.filter((r) => r !== null);
|
|
143
|
-
return { resources };
|
|
144
|
-
}
|
|
145
|
-
function normalizeSubscriptionUri(uri) {
|
|
146
|
-
const parsedUri = URL.parse(uri);
|
|
147
|
-
if (!parsedUri) {
|
|
148
|
-
throw new McpError(ErrorCode.InvalidParams, 'Invalid resource URI');
|
|
149
|
-
}
|
|
150
|
-
const cacheParts = parseCacheResourceFromUri(parsedUri);
|
|
151
|
-
if (cacheParts)
|
|
152
|
-
return toCacheResourceUri(cacheParts);
|
|
153
|
-
return parsedUri.href;
|
|
154
|
-
}
|
|
155
|
-
const cacheNotificationServers = new WeakSet();
|
|
156
|
-
function registerCacheResourceNotifications(server) {
|
|
157
|
-
if (cacheNotificationServers.has(server))
|
|
158
|
-
return;
|
|
159
|
-
cacheNotificationServers.add(server);
|
|
160
|
-
const subscribedResourceUris = new Set();
|
|
161
|
-
const setSubscription = (uri, subscribed) => {
|
|
162
|
-
const normalized = normalizeSubscriptionUri(uri);
|
|
163
|
-
if (subscribed) {
|
|
164
|
-
subscribedResourceUris.add(normalized);
|
|
165
|
-
}
|
|
166
|
-
else {
|
|
167
|
-
subscribedResourceUris.delete(normalized);
|
|
168
|
-
}
|
|
169
|
-
};
|
|
170
|
-
server.server.setRequestHandler(SubscribeRequestSchema, async (request) => {
|
|
171
|
-
setSubscription(request.params.uri, true);
|
|
172
|
-
return Promise.resolve({});
|
|
173
|
-
});
|
|
174
|
-
server.server.setRequestHandler(UnsubscribeRequestSchema, async (request) => {
|
|
175
|
-
setSubscription(request.params.uri, false);
|
|
176
|
-
return Promise.resolve({});
|
|
177
|
-
});
|
|
178
|
-
const unsubscribe = onCacheUpdate((event) => {
|
|
179
|
-
const scopeId = getServerCacheScopeId(server);
|
|
180
|
-
const changedUri = toCacheResourceUri({
|
|
181
|
-
namespace: event.namespace,
|
|
182
|
-
hash: event.urlHash,
|
|
183
|
-
});
|
|
184
|
-
const isVisibleToServer = event.scopeIds.includes(scopeId);
|
|
185
|
-
if (isVisibleToServer &&
|
|
186
|
-
server.isConnected() &&
|
|
187
|
-
subscribedResourceUris.has(changedUri)) {
|
|
188
|
-
void server.server
|
|
189
|
-
.sendResourceUpdated({ uri: changedUri })
|
|
190
|
-
.catch((error) => {
|
|
191
|
-
logWarn('Failed to send resource updated notification', {
|
|
192
|
-
uri: changedUri,
|
|
193
|
-
error,
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
if (!event.listChanged)
|
|
198
|
-
return;
|
|
199
|
-
if (!server.isConnected() || !isVisibleToServer)
|
|
200
|
-
return;
|
|
201
|
-
try {
|
|
202
|
-
server.sendResourceListChanged();
|
|
203
|
-
}
|
|
204
|
-
catch (error) {
|
|
205
|
-
logWarn('Failed to send resources list changed notification', { error });
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
let cleanedUp = false;
|
|
209
|
-
const cleanup = () => {
|
|
210
|
-
if (cleanedUp)
|
|
211
|
-
return;
|
|
212
|
-
cleanedUp = true;
|
|
213
|
-
unsubscribe();
|
|
214
|
-
};
|
|
215
|
-
registerServerLifecycleCleanup(server, cleanup);
|
|
216
|
-
}
|
|
217
|
-
function normalizeTemplateVariables(variables) {
|
|
218
|
-
if (!isObject(variables))
|
|
219
|
-
return {};
|
|
220
|
-
const normalized = {};
|
|
221
|
-
for (const [key, value] of Object.entries(variables)) {
|
|
222
|
-
if (typeof value === 'string' || value === undefined) {
|
|
223
|
-
normalized[key] = value;
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
if (Array.isArray(value)) {
|
|
227
|
-
normalized[key] = value.filter((item) => typeof item === 'string');
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
return normalized;
|
|
231
|
-
}
|
|
232
|
-
function resolveCacheResourceParts(uri, variables) {
|
|
233
|
-
const fromVariables = parseCacheResourceFromVariables(variables);
|
|
234
|
-
if (fromVariables)
|
|
235
|
-
return fromVariables;
|
|
236
|
-
const fromUri = parseCacheResourceFromUri(uri);
|
|
237
|
-
if (fromUri)
|
|
238
|
-
return fromUri;
|
|
239
|
-
throw new McpError(ErrorCode.InvalidParams, 'Invalid cache resource URI or template arguments');
|
|
240
|
-
}
|
|
241
|
-
export function readCacheResourceForScope(uri, variables, scopeId) {
|
|
242
|
-
const parts = resolveCacheResourceParts(uri, variables);
|
|
243
|
-
const cacheKey = `${parts.namespace}:${parts.hash}`;
|
|
244
|
-
const meta = getEntryMeta(cacheKey);
|
|
245
|
-
if (!meta || !isCacheEntryVisibleToScope(scopeId, meta)) {
|
|
246
|
-
throw new McpError(RESOURCE_NOT_FOUND_ERROR_CODE, 'Resource not found', {
|
|
247
|
-
uri: uri.href,
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
const entry = getCacheEntry(cacheKey);
|
|
251
|
-
if (!entry) {
|
|
252
|
-
throw new McpError(RESOURCE_NOT_FOUND_ERROR_CODE, 'Resource not found', {
|
|
253
|
-
uri: uri.href,
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
const payload = parseCachedPayload(entry.content);
|
|
257
|
-
const markdown = payload ? resolveCachedPayloadContent(payload) : null;
|
|
258
|
-
const text = markdown ?? entry.content;
|
|
259
|
-
return {
|
|
260
|
-
contents: [
|
|
261
|
-
{
|
|
262
|
-
uri: uri.href,
|
|
263
|
-
mimeType: 'text/markdown',
|
|
264
|
-
text,
|
|
265
|
-
},
|
|
266
|
-
],
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
4
|
export function registerInstructionResource(server, instructions, iconInfo) {
|
|
270
5
|
server.registerResource('fetch-url-mcp-instructions', 'internal://instructions', {
|
|
271
6
|
title: 'Server Instructions',
|
|
@@ -286,48 +21,22 @@ export function registerInstructionResource(server, instructions, iconInfo) {
|
|
|
286
21
|
],
|
|
287
22
|
}));
|
|
288
23
|
}
|
|
289
|
-
export function registerCacheResourceTemplate(server, iconInfo) {
|
|
290
|
-
const template = new ResourceTemplate(CACHE_RESOURCE_TEMPLATE_URI, {
|
|
291
|
-
list: () => listCacheResourcesForScope(getServerCacheScopeId(server)),
|
|
292
|
-
complete: {
|
|
293
|
-
namespace: (value) => completeCacheNamespaces(value, getServerCacheScopeId(server)),
|
|
294
|
-
hash: (value, context) => completeCacheHashes(value, getServerCacheScopeId(server), context),
|
|
295
|
-
},
|
|
296
|
-
});
|
|
297
|
-
server.registerResource('fetch-url-mcp-cache-entry', template, {
|
|
298
|
-
title: 'Cached Fetch Output',
|
|
299
|
-
description: 'Read cached markdown generated by previous fetch-url calls.',
|
|
300
|
-
mimeType: 'text/markdown',
|
|
301
|
-
annotations: {
|
|
302
|
-
audience: ['assistant'],
|
|
303
|
-
priority: 0.6,
|
|
304
|
-
},
|
|
305
|
-
...buildOptionalIcons(iconInfo),
|
|
306
|
-
}, (uri, variables) => readCacheResourceForScope(uri, normalizeTemplateVariables(variables), getServerCacheScopeId(server)));
|
|
307
|
-
registerCacheResourceNotifications(server);
|
|
308
|
-
}
|
|
309
24
|
export function buildServerInstructions() {
|
|
310
25
|
const maxHtmlSizeMb = config.constants.maxHtmlBytes / 1024 / 1024;
|
|
311
|
-
const cacheSizeMb = config.cache.maxSizeBytes / 1024 / 1024;
|
|
312
|
-
const cacheTtlHours = config.cache.ttl / 3600;
|
|
313
26
|
return `# Fetch public webpages and return clean, readable Markdown.
|
|
314
27
|
|
|
315
28
|
# Capabilities
|
|
316
29
|
- Tool: \`${FETCH_URL_TOOL_NAME}\` (fetch URL, return Markdown)
|
|
317
30
|
- Resource: \`internal://instructions\` (this document)
|
|
318
|
-
- Resource template: \`internal://cache/{namespace}/{hash}\` (cached Markdown)
|
|
319
31
|
- Prompt: \`get-help\` (returns these instructions)
|
|
320
|
-
- Completions: resource-template argument completion for cache entries
|
|
321
32
|
|
|
322
33
|
# Workflows
|
|
323
34
|
1. Standard: Call \`${FETCH_URL_TOOL_NAME}\` → read \`markdown\`. \`truncated: true\` means content was cut at server size limit.
|
|
324
|
-
2.
|
|
325
|
-
3. Async: \`task: { ttl: <ms> }\` in \`tools/call\` → poll \`tasks/get\` → \`tasks/result\`.
|
|
35
|
+
2. Async: \`task: { ttl: <ms> }\` in \`tools/call\` → poll \`tasks/get\` → \`tasks/result\`.
|
|
326
36
|
|
|
327
37
|
# Constraints
|
|
328
38
|
- Blocked URLs: localhost, private IPs (10.x, 172.16-31.x, 192.168.x), metadata (169.254.169.254), .local/.internal.
|
|
329
39
|
- Max HTML: ${maxHtmlSizeMb}MB. Max redirects: ${config.fetcher.maxRedirects}.
|
|
330
|
-
- Cache: ${config.cache.maxKeys} entries, ${cacheSizeMb}MB, ${cacheTtlHours}h TTL. Process-local, ephemeral.
|
|
331
40
|
- No JS rendering — client-side pages may be incomplete.
|
|
332
41
|
- Binary: not supported.
|
|
333
42
|
- Batch JSON-RPC (\`[{...}]\`): rejected with HTTP 400.
|
|
@@ -343,7 +52,7 @@ export function buildServerInstructions() {
|
|
|
343
52
|
- queue_full: worker pool busy. Wait and retry, or use task mode.`;
|
|
344
53
|
}
|
|
345
54
|
export function registerGetHelpPrompt(server, instructions, iconInfo) {
|
|
346
|
-
const description = 'Return Fetch URL server instructions: workflows,
|
|
55
|
+
const description = 'Return Fetch URL server instructions: workflows, task mode, and error handling.';
|
|
347
56
|
server.registerPrompt('get-help', {
|
|
348
57
|
title: 'Get Help',
|
|
349
58
|
description,
|
package/dist/schemas.d.ts
CHANGED
|
@@ -3,16 +3,8 @@ import type { ExtractedMetadata } from './transform/types.js';
|
|
|
3
3
|
export declare const extractedMetadataSchema: z.ZodType<ExtractedMetadata>;
|
|
4
4
|
export declare function normalizeExtractedMetadata(value: unknown): ExtractedMetadata | undefined;
|
|
5
5
|
export declare function normalizePageTitle(value: unknown): string | undefined;
|
|
6
|
-
export interface CachedPayload {
|
|
7
|
-
markdown: string;
|
|
8
|
-
title?: string | undefined;
|
|
9
|
-
metadata?: ExtractedMetadata | undefined;
|
|
10
|
-
truncated?: boolean | undefined;
|
|
11
|
-
}
|
|
12
|
-
export declare const cachedPayloadValueSchema: z.ZodType<CachedPayload>;
|
|
13
6
|
export declare const fetchUrlInputSchema: z.ZodObject<{
|
|
14
7
|
url: z.ZodURL;
|
|
15
|
-
forceRefresh: z.ZodOptional<z.ZodBoolean>;
|
|
16
8
|
}, z.core.$strict>;
|
|
17
9
|
export declare const fetchUrlOutputSchema: z.ZodObject<{
|
|
18
10
|
url: z.ZodURL;
|
|
@@ -22,14 +14,8 @@ export declare const fetchUrlOutputSchema: z.ZodObject<{
|
|
|
22
14
|
title: z.ZodOptional<z.ZodString>;
|
|
23
15
|
metadata: z.ZodOptional<z.ZodType<ExtractedMetadata, unknown, z.core.$ZodTypeInternals<ExtractedMetadata, unknown>>>;
|
|
24
16
|
markdown: z.ZodOptional<z.ZodString>;
|
|
25
|
-
fromCache: z.ZodOptional<z.ZodBoolean>;
|
|
26
17
|
fetchedAt: z.ZodOptional<z.ZodISODateTime>;
|
|
27
18
|
contentSize: z.ZodOptional<z.ZodNumber>;
|
|
28
19
|
truncated: z.ZodOptional<z.ZodBoolean>;
|
|
29
20
|
}, z.core.$strict>;
|
|
30
|
-
export declare function parseCachedPayload(raw: string): CachedPayload | null;
|
|
31
|
-
export declare function stringifyCachedPayload(payload: z.input<typeof cachedPayloadValueSchema>): string;
|
|
32
|
-
export declare function resolveCachedPayloadContent(payload: {
|
|
33
|
-
markdown?: string | null;
|
|
34
|
-
}): string | null;
|
|
35
21
|
//# sourceMappingURL=schemas.d.ts.map
|
package/dist/schemas.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAoD9D,eAAO,MAAM,uBAAuB,EAAE,CAAC,CAAC,OAAO,CAAC,iBAAiB,CAG9D,CAAC;AAgBJ,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,OAAO,GACb,iBAAiB,GAAG,SAAS,CAQ/B;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAErE;
|
|
1
|
+
{"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAoD9D,eAAO,MAAM,uBAAuB,EAAE,CAAC,CAAC,OAAO,CAAC,iBAAiB,CAG9D,CAAC;AAgBJ,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,OAAO,GACb,iBAAiB,GAAG,SAAS,CAQ/B;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAErE;AAED,eAAO,MAAM,mBAAmB;;kBAc/B,CAAC;AAEF,eAAO,MAAM,oBAAoB;;;;;;;;;;;kBAoC/B,CAAC"}
|
package/dist/schemas.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { config
|
|
2
|
+
import { config } from './lib/core.js';
|
|
3
3
|
const URL_FIELD_MAX_LENGTH = 2048;
|
|
4
4
|
const METADATA_LIMITS = {
|
|
5
5
|
title: 512,
|
|
@@ -49,61 +49,12 @@ export function normalizeExtractedMetadata(value) {
|
|
|
49
49
|
export function normalizePageTitle(value) {
|
|
50
50
|
return normalizeWithSchema(pageTitleSchema, value);
|
|
51
51
|
}
|
|
52
|
-
function normalizeString(value) {
|
|
53
|
-
return typeof value === 'string' ? value : undefined;
|
|
54
|
-
}
|
|
55
|
-
function normalizeBoolean(value) {
|
|
56
|
-
return typeof value === 'boolean' ? value : undefined;
|
|
57
|
-
}
|
|
58
|
-
export const cachedPayloadValueSchema = z.strictObject({
|
|
59
|
-
markdown: z.string(),
|
|
60
|
-
title: pageTitleSchema.optional(),
|
|
61
|
-
metadata: extractedMetadataSchema.optional(),
|
|
62
|
-
truncated: z.boolean().optional(),
|
|
63
|
-
});
|
|
64
|
-
const cachedPayloadCompatSchema = z.object({
|
|
65
|
-
markdown: z.unknown().transform((value) => normalizeString(value)),
|
|
66
|
-
title: z
|
|
67
|
-
.unknown()
|
|
68
|
-
.transform((value) => normalizePageTitle(value))
|
|
69
|
-
.optional(),
|
|
70
|
-
metadata: z
|
|
71
|
-
.unknown()
|
|
72
|
-
.transform((value) => normalizeExtractedMetadata(value))
|
|
73
|
-
.optional(),
|
|
74
|
-
truncated: z
|
|
75
|
-
.unknown()
|
|
76
|
-
.transform((value) => normalizeBoolean(value))
|
|
77
|
-
.optional(),
|
|
78
|
-
});
|
|
79
|
-
const cachedPayloadSchema = cachedPayloadCompatSchema
|
|
80
|
-
.superRefine((value, ctx) => {
|
|
81
|
-
if (typeof value.markdown === 'string')
|
|
82
|
-
return;
|
|
83
|
-
ctx.addIssue({
|
|
84
|
-
code: 'custom',
|
|
85
|
-
message: 'Missing markdown',
|
|
86
|
-
path: ['markdown'],
|
|
87
|
-
});
|
|
88
|
-
})
|
|
89
|
-
.transform((value) => cachedPayloadValueSchema.parse({
|
|
90
|
-
markdown: value.markdown,
|
|
91
|
-
...(value.title !== undefined ? { title: value.title } : {}),
|
|
92
|
-
...(value.metadata ? { metadata: value.metadata } : {}),
|
|
93
|
-
...(value.truncated !== undefined
|
|
94
|
-
? { truncated: value.truncated }
|
|
95
|
-
: {}),
|
|
96
|
-
}));
|
|
97
52
|
export const fetchUrlInputSchema = z.strictObject({
|
|
98
53
|
url: z
|
|
99
54
|
.httpUrl('Expected HTTP or HTTPS URL')
|
|
100
55
|
.min(1, 'URL required')
|
|
101
56
|
.max(config.constants.maxUrlLength, `URL exceeds ${config.constants.maxUrlLength} chars`)
|
|
102
57
|
.describe(`Target URL. Max ${config.constants.maxUrlLength} chars. Example: https://example.com`),
|
|
103
|
-
forceRefresh: z
|
|
104
|
-
.boolean('Expected boolean')
|
|
105
|
-
.optional()
|
|
106
|
-
.describe('Bypass cache and fetch fresh content.'),
|
|
107
58
|
}, 'Invalid input');
|
|
108
59
|
export const fetchUrlOutputSchema = z.strictObject({
|
|
109
60
|
url: z.httpUrl().max(config.constants.maxUrlLength).describe('Fetched URL.'),
|
|
@@ -131,7 +82,6 @@ export const fetchUrlOutputSchema = z.strictObject({
|
|
|
131
82
|
: z.string())
|
|
132
83
|
.optional()
|
|
133
84
|
.describe('Extracted Markdown. May be truncated (check truncated field).'),
|
|
134
|
-
fromCache: z.boolean().optional().describe('True if served from cache.'),
|
|
135
85
|
fetchedAt: z.iso.datetime().optional().describe('ISO timestamp of fetch.'),
|
|
136
86
|
contentSize: z
|
|
137
87
|
.number()
|
|
@@ -142,29 +92,3 @@ export const fetchUrlOutputSchema = z.strictObject({
|
|
|
142
92
|
.describe('Markdown fragment size before final inline truncation.'),
|
|
143
93
|
truncated: z.boolean().optional().describe('True if markdown was truncated.'),
|
|
144
94
|
});
|
|
145
|
-
export function parseCachedPayload(raw) {
|
|
146
|
-
try {
|
|
147
|
-
const parsed = JSON.parse(raw);
|
|
148
|
-
const result = cachedPayloadSchema.safeParse(parsed);
|
|
149
|
-
if (!result.success) {
|
|
150
|
-
logWarn('Rejected invalid cached payload', {
|
|
151
|
-
issues: result.error.issues.map((issue) => ({
|
|
152
|
-
path: issue.path,
|
|
153
|
-
message: issue.message,
|
|
154
|
-
code: issue.code,
|
|
155
|
-
})),
|
|
156
|
-
});
|
|
157
|
-
return null;
|
|
158
|
-
}
|
|
159
|
-
return result.data;
|
|
160
|
-
}
|
|
161
|
-
catch {
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
export function stringifyCachedPayload(payload) {
|
|
166
|
-
return JSON.stringify(cachedPayloadValueSchema.parse(payload));
|
|
167
|
-
}
|
|
168
|
-
export function resolveCachedPayloadContent(payload) {
|
|
169
|
-
return typeof payload.markdown === 'string' ? payload.markdown : null;
|
|
170
|
-
}
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AA8GpE,wBAAsB,eAAe,IAAI,OAAO,CAAC,SAAS,CAAC,CAE1D;AAkDD,wBAAsB,6BAA6B,IAAI,OAAO,CAAC,SAAS,CAAC,CAExE;AA4FD,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAMtD"}
|
package/dist/server.js
CHANGED
|
@@ -7,7 +7,7 @@ import { config } from './lib/core.js';
|
|
|
7
7
|
import { getSessionId, logError, logInfo, setLogLevel, setMcpServer, } from './lib/core.js';
|
|
8
8
|
import { setTaskToolCallCapability } from './lib/mcp-interop.js';
|
|
9
9
|
import { toError } from './lib/utils.js';
|
|
10
|
-
import { buildServerInstructions,
|
|
10
|
+
import { buildServerInstructions, registerGetHelpPrompt, registerInstructionResource, } from './resources/index.js';
|
|
11
11
|
import { abortAllTaskExecutions, registerTaskHandlers, } from './tasks/handlers.js';
|
|
12
12
|
import { registerTools as registerFetchUrlTool } from './tools/fetch-url.js';
|
|
13
13
|
import { shutdownTransformWorkerPool } from './transform/transform.js';
|
|
@@ -89,7 +89,6 @@ async function createMcpServerWithOptions(options) {
|
|
|
89
89
|
const toolControls = registerFetchUrlTool(server);
|
|
90
90
|
registerGetHelpPrompt(server, serverInstructions, localIcon);
|
|
91
91
|
registerInstructionResource(server, serverInstructions, localIcon);
|
|
92
|
-
registerCacheResourceTemplate(server, localIcon);
|
|
93
92
|
const taskRegistration = registerTaskHandlers(server, {
|
|
94
93
|
requireInterception: config.tasks.requireInterception,
|
|
95
94
|
});
|
|
@@ -18,13 +18,11 @@ export declare function getFetchCompletionStatusMessage(result: ServerResult): s
|
|
|
18
18
|
export declare class FetchUrlProgressPlan {
|
|
19
19
|
private readonly reporter;
|
|
20
20
|
private readonly context;
|
|
21
|
-
private cacheStatus;
|
|
22
21
|
constructor(reporter: ProgressReporter, context: string);
|
|
23
22
|
reportStart(): void;
|
|
24
23
|
reportStage(stage: SharedFetchStage): void;
|
|
25
24
|
reportSuccess(contentSize: number): void;
|
|
26
25
|
reportFailure(cancelled: boolean): void;
|
|
27
|
-
private updateCacheStatus;
|
|
28
26
|
private mapStage;
|
|
29
27
|
}
|
|
30
28
|
export declare function fetchUrlToolHandler(input: FetchUrlInput, extra?: ToolHandlerExtra): Promise<ToolResponseBase>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetch-url.d.ts","sourceRoot":"","sources":["../../src/tools/fetch-url.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EACV,YAAY,EAEb,MAAM,oCAAoC,CAAC;AAE5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"fetch-url.d.ts","sourceRoot":"","sources":["../../src/tools/fetch-url.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EACV,YAAY,EAEb,MAAM,oCAAoC,CAAC;AAE5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAY7B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACtB,MAAM,uBAAuB,CAAC;AAU/B,OAAO,EACL,mBAAmB,EAIpB,MAAM,eAAe,CAAC;AAEvB,OAAO,EAIL,KAAK,sBAAsB,EAE5B,MAAM,sBAAsB,CAAC;AAE9B,KAAK,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEzD,UAAU,gBAAgB;IACxB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IACvB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IACxD,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,eAAO,MAAM,mBAAmB,cAAc,CAAC;AAoI/C,wBAAgB,+BAA+B,CAC7C,MAAM,EAAE,YAAY,GACnB,MAAM,GAAG,SAAS,CAUpB;AAED,qBAAa,oBAAoB;IAE7B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBADP,QAAQ,EAAE,gBAAgB,EAC1B,OAAO,EAAE,MAAM;IAGlC,WAAW,IAAI,IAAI;IAInB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI;IAM1C,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;IAIxC,aAAa,CAAC,SAAS,EAAE,OAAO,GAAG,IAAI;IAIvC,OAAO,CAAC,QAAQ;CAqBjB;AAmED,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,aAAa,EACpB,KAAK,CAAC,EAAE,gBAAgB,GACvB,OAAO,CAAC,gBAAgB,CAAC,CAQ3B;AAqBD,MAAM,WAAW,wBAAwB;IACvC,cAAc,EAAE,CAAC,OAAO,EAAE,sBAAsB,KAAK,IAAI,CAAC;CAC3D;AAqBD,wBAAgB,aAAa,CAAC,MAAM,EAAE,SAAS,GAAG,wBAAwB,CAoCzE"}
|
package/dist/tools/fetch-url.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
2
2
|
import { config, logDebug, logError, logWarn } from '../lib/core.js';
|
|
3
|
-
import { finalizeInlineMarkdown, markdownTransform,
|
|
3
|
+
import { finalizeInlineMarkdown, markdownTransform, performSharedFetch, withSignal, } from '../lib/fetch-pipeline.js';
|
|
4
4
|
import { createProgressReporter, handleToolError, } from '../lib/mcp-interop.js';
|
|
5
5
|
import { composeAbortSignal, isAbortError, isObject, parseUrlOrNull, toError, } from '../lib/utils.js';
|
|
6
6
|
import { formatZodError } from '../lib/zod.js';
|
|
@@ -57,7 +57,6 @@ function buildStructuredContent(pipeline, inlineResult, inputUrl) {
|
|
|
57
57
|
...(title ? { title } : {}),
|
|
58
58
|
...(metadata ? { metadata } : {}),
|
|
59
59
|
markdown,
|
|
60
|
-
fromCache: pipeline.fromCache,
|
|
61
60
|
fetchedAt: pipeline.fetchedAt,
|
|
62
61
|
contentSize: inlineResult.contentSize,
|
|
63
62
|
...(truncated ? { truncated: true } : {}),
|
|
@@ -88,12 +87,11 @@ function buildResponse(pipeline, inlineResult, inputUrl) {
|
|
|
88
87
|
const Step = {
|
|
89
88
|
START: 1,
|
|
90
89
|
RESOLVE_URL: 2,
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
DONE: 8,
|
|
90
|
+
FETCH: 3,
|
|
91
|
+
RESPONSE: 4,
|
|
92
|
+
TRANSFORM: 5,
|
|
93
|
+
PREPARE: 6,
|
|
94
|
+
DONE: 7,
|
|
97
95
|
};
|
|
98
96
|
function formatContentSize(contentSize) {
|
|
99
97
|
if (contentSize < 1000)
|
|
@@ -119,7 +117,6 @@ export function getFetchCompletionStatusMessage(result) {
|
|
|
119
117
|
export class FetchUrlProgressPlan {
|
|
120
118
|
reporter;
|
|
121
119
|
context;
|
|
122
|
-
cacheStatus = 'unknown';
|
|
123
120
|
constructor(reporter, context) {
|
|
124
121
|
this.reporter = reporter;
|
|
125
122
|
this.context = context;
|
|
@@ -128,7 +125,6 @@ export class FetchUrlProgressPlan {
|
|
|
128
125
|
this.reporter.report(Step.START, 'Preparing request');
|
|
129
126
|
}
|
|
130
127
|
reportStage(stage) {
|
|
131
|
-
this.updateCacheStatus(stage);
|
|
132
128
|
const mapped = this.mapStage(stage);
|
|
133
129
|
if (!mapped)
|
|
134
130
|
return;
|
|
@@ -140,47 +136,23 @@ export class FetchUrlProgressPlan {
|
|
|
140
136
|
reportFailure(cancelled) {
|
|
141
137
|
this.reporter.report(Step.DONE, cancelled ? 'Cancelled' : 'Failed');
|
|
142
138
|
}
|
|
143
|
-
updateCacheStatus(stage) {
|
|
144
|
-
if (stage === 'cache_hit' || stage === 'cache_restore') {
|
|
145
|
-
this.cacheStatus = 'cache_hit';
|
|
146
|
-
}
|
|
147
|
-
else if (stage === 'fetch_remote' ||
|
|
148
|
-
stage === 'response_ready' ||
|
|
149
|
-
stage === 'transform_start') {
|
|
150
|
-
this.cacheStatus = 'cache_miss';
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
139
|
mapStage(stage) {
|
|
154
140
|
switch (stage) {
|
|
155
141
|
case 'resolve_url':
|
|
156
142
|
return { step: Step.RESOLVE_URL, message: 'Resolving URL' };
|
|
157
|
-
case 'check_cache':
|
|
158
|
-
return { step: Step.CHECK_CACHE, message: 'Checking cache' };
|
|
159
|
-
case 'cache_hit':
|
|
160
|
-
return { step: Step.CACHE_OR_FETCH, message: 'Loaded from cache' };
|
|
161
|
-
case 'cache_restore':
|
|
162
|
-
return {
|
|
163
|
-
step: Step.RESTORE_OR_RESPONSE,
|
|
164
|
-
message: 'Restoring cached content',
|
|
165
|
-
};
|
|
166
143
|
case 'fetch_remote':
|
|
167
144
|
return {
|
|
168
|
-
step: Step.
|
|
145
|
+
step: Step.FETCH,
|
|
169
146
|
message: `Fetching ${this.context}`,
|
|
170
147
|
};
|
|
171
148
|
case 'response_ready':
|
|
172
|
-
return { step: Step.
|
|
149
|
+
return { step: Step.RESPONSE, message: 'Received response' };
|
|
173
150
|
case 'transform_start':
|
|
174
151
|
return { step: Step.TRANSFORM, message: 'Parsing HTML -> Markdown' };
|
|
175
152
|
case 'prepare_output':
|
|
176
|
-
return {
|
|
177
|
-
step: this.cacheStatus === 'cache_miss' ? Step.PREPARE : Step.TRANSFORM,
|
|
178
|
-
message: 'Fetch completed',
|
|
179
|
-
};
|
|
153
|
+
return { step: Step.PREPARE, message: 'Fetch completed' };
|
|
180
154
|
case 'finalize_output':
|
|
181
|
-
|
|
182
|
-
return undefined;
|
|
183
|
-
return { step: Step.PREPARE, message: 'Finalizing output' };
|
|
155
|
+
return undefined;
|
|
184
156
|
}
|
|
185
157
|
}
|
|
186
158
|
}
|
|
@@ -195,19 +167,16 @@ function buildToolAbortSignal(extraSignal) {
|
|
|
195
167
|
}
|
|
196
168
|
return signal;
|
|
197
169
|
}
|
|
198
|
-
function buildFetchOptions(url, signal, progressPlan
|
|
170
|
+
function buildFetchOptions(url, signal, progressPlan) {
|
|
199
171
|
return {
|
|
200
172
|
url,
|
|
201
173
|
...withSignal(signal),
|
|
202
|
-
...(forceRefresh ? { forceRefresh: true } : {}),
|
|
203
174
|
onStage: (stage) => {
|
|
204
175
|
progressPlan.reportStage(stage);
|
|
205
176
|
},
|
|
206
177
|
transform: async ({ buffer, encoding, truncated }, normalizedUrl) => {
|
|
207
178
|
return markdownTransform({ buffer, encoding, ...(truncated ? { truncated } : {}) }, normalizedUrl, signal);
|
|
208
179
|
},
|
|
209
|
-
serialize: serializeMarkdownResult,
|
|
210
|
-
deserialize: parseCachedMarkdownResult,
|
|
211
180
|
};
|
|
212
181
|
}
|
|
213
182
|
async function executeFetch(input, extra) {
|
|
@@ -217,7 +186,7 @@ async function executeFetch(input, extra) {
|
|
|
217
186
|
logDebug('Fetching URL', { url });
|
|
218
187
|
try {
|
|
219
188
|
progressPlan.reportStart();
|
|
220
|
-
const { pipeline, inlineResult } = await performSharedFetch(buildFetchOptions(url, signal, progressPlan
|
|
189
|
+
const { pipeline, inlineResult } = await performSharedFetch(buildFetchOptions(url, signal, progressPlan));
|
|
221
190
|
progressPlan.reportSuccess(inlineResult.contentSize);
|
|
222
191
|
return buildResponse(pipeline, inlineResult, url);
|
|
223
192
|
}
|