@j0hanz/superfetch 2.4.3 → 2.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cache.d.ts +8 -8
- package/dist/cache.js +277 -264
- package/dist/crypto.js +4 -3
- package/dist/dom-noise-removal.js +355 -297
- package/dist/fetch.d.ts +13 -7
- package/dist/fetch.js +636 -690
- package/dist/http-native.js +535 -474
- package/dist/instructions.md +38 -27
- package/dist/language-detection.js +190 -153
- package/dist/markdown-cleanup.js +171 -158
- package/dist/mcp.js +161 -1
- package/dist/resources.d.ts +2 -0
- package/dist/resources.js +44 -0
- package/dist/session.js +144 -105
- package/dist/tasks.d.ts +37 -0
- package/dist/tasks.js +66 -0
- package/dist/tools.d.ts +6 -9
- package/dist/tools.js +166 -136
- package/dist/transform.d.ts +3 -1
- package/dist/transform.js +680 -778
- package/package.json +6 -6
package/dist/markdown-cleanup.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { config } from './config.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
/* -------------------------------------------------------------------------------------------------
|
|
3
|
+
* Fences
|
|
4
|
+
* ------------------------------------------------------------------------------------------------- */
|
|
5
5
|
function isFenceStart(line) {
|
|
6
6
|
const trimmed = line.trimStart();
|
|
7
7
|
return trimmed.startsWith('```') || trimmed.startsWith('~~~');
|
|
@@ -29,49 +29,121 @@ function advanceFenceState(line, state) {
|
|
|
29
29
|
state.marker = '';
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
current
|
|
32
|
+
class FencedSegmenter {
|
|
33
|
+
split(content) {
|
|
34
|
+
const lines = content.split('\n');
|
|
35
|
+
const segments = [];
|
|
36
|
+
const state = initialFenceState();
|
|
37
|
+
let current = [];
|
|
38
|
+
let currentIsFence = false;
|
|
39
|
+
for (const line of lines) {
|
|
40
|
+
// Transition into fence: flush outside segment first.
|
|
41
|
+
if (!state.inFence && isFenceStart(line)) {
|
|
42
|
+
if (current.length > 0) {
|
|
43
|
+
segments.push({
|
|
44
|
+
content: current.join('\n'),
|
|
45
|
+
inFence: currentIsFence,
|
|
46
|
+
});
|
|
47
|
+
current = [];
|
|
48
|
+
}
|
|
49
|
+
currentIsFence = true;
|
|
50
|
+
current.push(line);
|
|
51
|
+
advanceFenceState(line, state);
|
|
52
|
+
continue;
|
|
51
53
|
}
|
|
52
|
-
currentIsFence = true;
|
|
53
54
|
current.push(line);
|
|
55
|
+
const wasInFence = state.inFence;
|
|
54
56
|
advanceFenceState(line, state);
|
|
55
|
-
|
|
57
|
+
// Transition out of fence: flush fence segment.
|
|
58
|
+
if (wasInFence && !state.inFence) {
|
|
59
|
+
segments.push({ content: current.join('\n'), inFence: true });
|
|
60
|
+
current = [];
|
|
61
|
+
currentIsFence = false;
|
|
62
|
+
}
|
|
56
63
|
}
|
|
57
|
-
current.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
if (current.length > 0) {
|
|
65
|
+
segments.push({ content: current.join('\n'), inFence: currentIsFence });
|
|
66
|
+
}
|
|
67
|
+
return segments;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const fencedSegmenter = new FencedSegmenter();
|
|
71
|
+
/* -------------------------------------------------------------------------------------------------
|
|
72
|
+
* Orphan heading promotion
|
|
73
|
+
* ------------------------------------------------------------------------------------------------- */
|
|
74
|
+
const HEADING_KEYWORDS = new Set([
|
|
75
|
+
'overview',
|
|
76
|
+
'introduction',
|
|
77
|
+
'summary',
|
|
78
|
+
'conclusion',
|
|
79
|
+
'prerequisites',
|
|
80
|
+
'requirements',
|
|
81
|
+
'installation',
|
|
82
|
+
'configuration',
|
|
83
|
+
'usage',
|
|
84
|
+
'features',
|
|
85
|
+
'limitations',
|
|
86
|
+
'troubleshooting',
|
|
87
|
+
'faq',
|
|
88
|
+
'resources',
|
|
89
|
+
'references',
|
|
90
|
+
'changelog',
|
|
91
|
+
'license',
|
|
92
|
+
'acknowledgments',
|
|
93
|
+
'appendix',
|
|
94
|
+
]);
|
|
95
|
+
class OrphanHeadingPromoter {
|
|
96
|
+
shouldPromote(line, prevLine) {
|
|
97
|
+
const isPrecededByBlank = prevLine.trim() === '';
|
|
98
|
+
if (!isPrecededByBlank)
|
|
99
|
+
return false;
|
|
100
|
+
return this.isLikelyHeadingLine(line);
|
|
101
|
+
}
|
|
102
|
+
format(line) {
|
|
103
|
+
const trimmed = line.trim();
|
|
104
|
+
const isExample = /^example:\s/i.test(trimmed);
|
|
105
|
+
const prefix = isExample ? '### ' : '## ';
|
|
106
|
+
return prefix + trimmed;
|
|
107
|
+
}
|
|
108
|
+
processLine(line, prevLine) {
|
|
109
|
+
if (this.shouldPromote(line, prevLine)) {
|
|
110
|
+
return this.format(line);
|
|
65
111
|
}
|
|
112
|
+
return line;
|
|
66
113
|
}
|
|
67
|
-
|
|
68
|
-
|
|
114
|
+
isLikelyHeadingLine(line) {
|
|
115
|
+
const trimmed = line.trim();
|
|
116
|
+
if (!trimmed || trimmed.length > 80)
|
|
117
|
+
return false;
|
|
118
|
+
if (/^#{1,6}\s/.test(trimmed))
|
|
119
|
+
return false;
|
|
120
|
+
if (/^[-*+•]\s/.test(trimmed) || /^\d+\.\s/.test(trimmed))
|
|
121
|
+
return false;
|
|
122
|
+
if (/[.!?]$/.test(trimmed))
|
|
123
|
+
return false;
|
|
124
|
+
if (/^\[.*\]\(.*\)$/.test(trimmed))
|
|
125
|
+
return false;
|
|
126
|
+
if (/^(?:example|note|tip|warning|important|caution):\s+\S/i.test(trimmed)) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
const words = trimmed.split(/\s+/);
|
|
130
|
+
if (words.length >= 2 && words.length <= 6) {
|
|
131
|
+
const isTitleCase = words.every((w) => /^[A-Z][a-z]*$/.test(w) || /^(?:and|or|the|of|in|for|to|a)$/i.test(w));
|
|
132
|
+
if (isTitleCase)
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
if (words.length === 1) {
|
|
136
|
+
const lower = trimmed.toLowerCase();
|
|
137
|
+
if (HEADING_KEYWORDS.has(lower) && /^[A-Z]/.test(trimmed))
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
69
141
|
}
|
|
70
|
-
return segments;
|
|
71
142
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
143
|
+
const orphanHeadingPromoter = new OrphanHeadingPromoter();
|
|
144
|
+
/* -------------------------------------------------------------------------------------------------
|
|
145
|
+
* Cleanup rules (OUTSIDE fences only)
|
|
146
|
+
* ------------------------------------------------------------------------------------------------- */
|
|
75
147
|
function removeEmptyHeadings(text) {
|
|
76
148
|
return text.replace(/^#{1,6}[ \t\u00A0]*$\r?\n?/gm, '');
|
|
77
149
|
}
|
|
@@ -88,7 +160,7 @@ function fixOrphanHeadings(text) {
|
|
|
88
160
|
});
|
|
89
161
|
}
|
|
90
162
|
function removeSkipLinksAndEmptyAnchors(text) {
|
|
91
|
-
const zeroWidthAnchorLink = /\[(?:\s|\u200B)*\]\(#[^)]*\)\
|
|
163
|
+
const zeroWidthAnchorLink = /\[(?:\s|\u200B)*\]\(#[^)]*\)[ \t]*/g;
|
|
92
164
|
return text
|
|
93
165
|
.replace(zeroWidthAnchorLink, '')
|
|
94
166
|
.replace(/^\[Skip to (?:main )?content\]\(#[^)]*\)\s*$/gim, '')
|
|
@@ -174,39 +246,43 @@ const CLEANUP_STEPS = [
|
|
|
174
246
|
normalizeListsAndSpacing,
|
|
175
247
|
fixConcatenatedProperties,
|
|
176
248
|
];
|
|
177
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
178
|
-
// Public API
|
|
179
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
180
249
|
function getLastLine(text) {
|
|
181
250
|
const index = text.lastIndexOf('\n');
|
|
182
251
|
return index === -1 ? text : text.slice(index + 1);
|
|
183
252
|
}
|
|
253
|
+
class MarkdownCleanupPipeline {
|
|
254
|
+
cleanup(markdown) {
|
|
255
|
+
if (!markdown)
|
|
256
|
+
return '';
|
|
257
|
+
const segments = fencedSegmenter.split(markdown);
|
|
258
|
+
const cleaned = segments
|
|
259
|
+
.map((seg, index) => {
|
|
260
|
+
if (seg.inFence)
|
|
261
|
+
return seg.content;
|
|
262
|
+
const prevSeg = segments[index - 1];
|
|
263
|
+
const prevLineContext = prevSeg ? getLastLine(prevSeg.content) : '';
|
|
264
|
+
const lines = seg.content.split('\n');
|
|
265
|
+
const promotedLines = [];
|
|
266
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
267
|
+
const line = lines[i] ?? '';
|
|
268
|
+
const prevLine = i > 0 ? (lines[i - 1] ?? '') : prevLineContext;
|
|
269
|
+
promotedLines.push(orphanHeadingPromoter.processLine(line, prevLine));
|
|
270
|
+
}
|
|
271
|
+
const promoted = promotedLines.join('\n');
|
|
272
|
+
return CLEANUP_STEPS.reduce((text, step) => step(text), promoted);
|
|
273
|
+
})
|
|
274
|
+
.join('\n')
|
|
275
|
+
.trim();
|
|
276
|
+
return cleaned;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const markdownCleanupPipeline = new MarkdownCleanupPipeline();
|
|
184
280
|
export function cleanupMarkdownArtifacts(content) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (seg.inFence)
|
|
191
|
-
return seg.content;
|
|
192
|
-
const prevSeg = segments[index - 1];
|
|
193
|
-
const prevLineContext = prevSeg ? getLastLine(prevSeg.content) : '';
|
|
194
|
-
const lines = seg.content.split('\n');
|
|
195
|
-
const promotedLines = [];
|
|
196
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
197
|
-
const line = lines[i] ?? '';
|
|
198
|
-
const prevLine = i > 0 ? (lines[i - 1] ?? '') : prevLineContext;
|
|
199
|
-
promotedLines.push(processNonFencedLine(line, prevLine));
|
|
200
|
-
}
|
|
201
|
-
const promoted = promotedLines.join('\n');
|
|
202
|
-
return CLEANUP_STEPS.reduce((text, step) => step(text), promoted);
|
|
203
|
-
})
|
|
204
|
-
.join('\n')
|
|
205
|
-
.trim();
|
|
206
|
-
}
|
|
207
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
208
|
-
// Raw markdown handling + metadata footer
|
|
209
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
281
|
+
return markdownCleanupPipeline.cleanup(content);
|
|
282
|
+
}
|
|
283
|
+
/* -------------------------------------------------------------------------------------------------
|
|
284
|
+
* Raw markdown handling + metadata footer
|
|
285
|
+
* ------------------------------------------------------------------------------------------------- */
|
|
210
286
|
const HEADING_PATTERN = /^#{1,6}\s/m;
|
|
211
287
|
const LIST_PATTERN = /^(?:[-*+])\s/m;
|
|
212
288
|
const HTML_DOCUMENT_PATTERN = /^(<!doctype|<html)/i;
|
|
@@ -231,16 +307,22 @@ function detectLineEnding(content) {
|
|
|
231
307
|
return content.includes('\r\n') ? '\r\n' : '\n';
|
|
232
308
|
}
|
|
233
309
|
const FRONTMATTER_DELIMITER = '---';
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
310
|
+
class RawMarkdownFrontmatter {
|
|
311
|
+
find(content) {
|
|
312
|
+
const lineEnding = detectLineEnding(content);
|
|
313
|
+
const lines = content.split(lineEnding);
|
|
314
|
+
if (lines[0] !== FRONTMATTER_DELIMITER)
|
|
315
|
+
return null;
|
|
316
|
+
const endIndex = lines.indexOf(FRONTMATTER_DELIMITER, 1);
|
|
317
|
+
if (endIndex === -1)
|
|
318
|
+
return null;
|
|
319
|
+
return { lineEnding, lines, endIndex };
|
|
320
|
+
}
|
|
321
|
+
hasFrontmatter(trimmed) {
|
|
322
|
+
return trimmed.startsWith('---\n') || trimmed.startsWith('---\r\n');
|
|
323
|
+
}
|
|
243
324
|
}
|
|
325
|
+
const frontmatter = new RawMarkdownFrontmatter();
|
|
244
326
|
function stripOptionalQuotes(value) {
|
|
245
327
|
const trimmed = value.trim();
|
|
246
328
|
if (trimmed.length < 2)
|
|
@@ -288,11 +370,11 @@ function extractTitleFromHeading(content) {
|
|
|
288
370
|
return undefined;
|
|
289
371
|
}
|
|
290
372
|
export function extractTitleFromRawMarkdown(content) {
|
|
291
|
-
const
|
|
292
|
-
if (!
|
|
373
|
+
const fm = frontmatter.find(content);
|
|
374
|
+
if (!fm) {
|
|
293
375
|
return extractTitleFromHeading(content);
|
|
294
376
|
}
|
|
295
|
-
const { lines, endIndex } =
|
|
377
|
+
const { lines, endIndex } = fm;
|
|
296
378
|
const entry = lines
|
|
297
379
|
.slice(1, endIndex)
|
|
298
380
|
.map((line) => parseFrontmatterEntry(line))
|
|
@@ -339,14 +421,15 @@ function addSourceToMarkdownMarkdownFormat(content, url) {
|
|
|
339
421
|
return [`Source: ${url}`, '', content].join(lineEnding);
|
|
340
422
|
}
|
|
341
423
|
export function addSourceToMarkdown(content, url) {
|
|
342
|
-
const
|
|
343
|
-
if (config.transform.metadataFormat === 'markdown' && !
|
|
424
|
+
const fm = frontmatter.find(content);
|
|
425
|
+
if (config.transform.metadataFormat === 'markdown' && !fm) {
|
|
344
426
|
return addSourceToMarkdownMarkdownFormat(content, url);
|
|
345
427
|
}
|
|
346
|
-
if (!
|
|
428
|
+
if (!fm) {
|
|
429
|
+
// Preserve existing behavior: always uses LF even if content uses CRLF.
|
|
347
430
|
return `---\nsource: "${url}"\n---\n\n${content}`;
|
|
348
431
|
}
|
|
349
|
-
const { lineEnding, lines, endIndex } =
|
|
432
|
+
const { lineEnding, lines, endIndex } = fm;
|
|
350
433
|
const bodyLines = lines.slice(1, endIndex);
|
|
351
434
|
const hasSource = bodyLines.some((line) => line.trimStart().toLowerCase().startsWith('source:'));
|
|
352
435
|
if (hasSource)
|
|
@@ -359,9 +442,6 @@ export function addSourceToMarkdown(content, url) {
|
|
|
359
442
|
];
|
|
360
443
|
return updatedLines.join(lineEnding);
|
|
361
444
|
}
|
|
362
|
-
function hasFrontmatter(trimmed) {
|
|
363
|
-
return trimmed.startsWith('---\n') || trimmed.startsWith('---\r\n');
|
|
364
|
-
}
|
|
365
445
|
function looksLikeHtmlDocument(trimmed) {
|
|
366
446
|
return HTML_DOCUMENT_PATTERN.test(trimmed);
|
|
367
447
|
}
|
|
@@ -373,7 +453,7 @@ function countCommonHtmlTags(content) {
|
|
|
373
453
|
export function isRawTextContent(content) {
|
|
374
454
|
const trimmed = content.trim();
|
|
375
455
|
const isHtmlDocument = looksLikeHtmlDocument(trimmed);
|
|
376
|
-
const hasMarkdownFrontmatter = hasFrontmatter(trimmed);
|
|
456
|
+
const hasMarkdownFrontmatter = frontmatter.hasFrontmatter(trimmed);
|
|
377
457
|
const hasTooManyHtmlTags = countCommonHtmlTags(content) > 2;
|
|
378
458
|
const isMarkdown = looksLikeMarkdown(content);
|
|
379
459
|
return (!isHtmlDocument &&
|
|
@@ -423,76 +503,9 @@ export function buildMetadataFooter(metadata, fallbackUrl) {
|
|
|
423
503
|
}
|
|
424
504
|
return lines.join('\n');
|
|
425
505
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
const HEADING_KEYWORDS = new Set([
|
|
430
|
-
'overview',
|
|
431
|
-
'introduction',
|
|
432
|
-
'summary',
|
|
433
|
-
'conclusion',
|
|
434
|
-
'prerequisites',
|
|
435
|
-
'requirements',
|
|
436
|
-
'installation',
|
|
437
|
-
'configuration',
|
|
438
|
-
'usage',
|
|
439
|
-
'features',
|
|
440
|
-
'limitations',
|
|
441
|
-
'troubleshooting',
|
|
442
|
-
'faq',
|
|
443
|
-
'resources',
|
|
444
|
-
'references',
|
|
445
|
-
'changelog',
|
|
446
|
-
'license',
|
|
447
|
-
'acknowledgments',
|
|
448
|
-
'appendix',
|
|
449
|
-
]);
|
|
450
|
-
function isLikelyHeadingLine(line) {
|
|
451
|
-
const trimmed = line.trim();
|
|
452
|
-
if (!trimmed || trimmed.length > 80)
|
|
453
|
-
return false;
|
|
454
|
-
if (/^#{1,6}\s/.test(trimmed))
|
|
455
|
-
return false;
|
|
456
|
-
if (/^[-*+•]\s/.test(trimmed) || /^\d+\.\s/.test(trimmed))
|
|
457
|
-
return false;
|
|
458
|
-
if (/[.!?]$/.test(trimmed))
|
|
459
|
-
return false;
|
|
460
|
-
if (/^\[.*\]\(.*\)$/.test(trimmed))
|
|
461
|
-
return false;
|
|
462
|
-
if (/^(?:example|note|tip|warning|important|caution):\s+\S/i.test(trimmed)) {
|
|
463
|
-
return true;
|
|
464
|
-
}
|
|
465
|
-
const words = trimmed.split(/\s+/);
|
|
466
|
-
if (words.length >= 2 && words.length <= 6) {
|
|
467
|
-
const isTitleCase = words.every((w) => /^[A-Z][a-z]*$/.test(w) || /^(?:and|or|the|of|in|for|to|a)$/i.test(w));
|
|
468
|
-
if (isTitleCase)
|
|
469
|
-
return true;
|
|
470
|
-
}
|
|
471
|
-
if (words.length === 1) {
|
|
472
|
-
const lower = trimmed.toLowerCase();
|
|
473
|
-
if (HEADING_KEYWORDS.has(lower) && /^[A-Z]/.test(trimmed))
|
|
474
|
-
return true;
|
|
475
|
-
}
|
|
476
|
-
return false;
|
|
477
|
-
}
|
|
478
|
-
function shouldPromoteToHeading(line, prevLine) {
|
|
479
|
-
const isPrecededByBlank = prevLine.trim() === '';
|
|
480
|
-
if (!isPrecededByBlank)
|
|
481
|
-
return false;
|
|
482
|
-
return isLikelyHeadingLine(line);
|
|
483
|
-
}
|
|
484
|
-
function formatAsHeading(line) {
|
|
485
|
-
const trimmed = line.trim();
|
|
486
|
-
const isExample = /^example:\s/i.test(trimmed);
|
|
487
|
-
const prefix = isExample ? '### ' : '## ';
|
|
488
|
-
return prefix + trimmed;
|
|
489
|
-
}
|
|
490
|
-
function processNonFencedLine(line, prevLine) {
|
|
491
|
-
if (shouldPromoteToHeading(line, prevLine)) {
|
|
492
|
-
return formatAsHeading(line);
|
|
493
|
-
}
|
|
494
|
-
return line;
|
|
495
|
-
}
|
|
506
|
+
/* -------------------------------------------------------------------------------------------------
|
|
507
|
+
* Heading promotion (fence-aware)
|
|
508
|
+
* ------------------------------------------------------------------------------------------------- */
|
|
496
509
|
/**
|
|
497
510
|
* Promote standalone lines that look like headings to proper markdown headings.
|
|
498
511
|
* Fence-aware: never modifies content inside fenced code blocks.
|
|
@@ -511,7 +524,7 @@ export function promoteOrphanHeadings(markdown) {
|
|
|
511
524
|
advanceFenceState(line, state);
|
|
512
525
|
continue;
|
|
513
526
|
}
|
|
514
|
-
result.push(
|
|
527
|
+
result.push(orphanHeadingPromoter.processLine(line, prevLine));
|
|
515
528
|
}
|
|
516
529
|
return result.join('\n');
|
|
517
530
|
}
|
package/dist/mcp.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { z } from 'zod';
|
|
2
3
|
import { McpServer, ResourceTemplate, } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
4
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
+
import { CallToolRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
4
6
|
import { registerCachedContentResource } from './cache.js';
|
|
5
7
|
import { config } from './config.js';
|
|
6
8
|
import { destroyAgents } from './fetch.js';
|
|
7
9
|
import { logError, logInfo, setMcpServer } from './observability.js';
|
|
8
|
-
import {
|
|
10
|
+
import { registerConfigResource } from './resources.js';
|
|
11
|
+
import { taskManager } from './tasks.js';
|
|
12
|
+
import { FETCH_URL_TOOL_NAME, fetchUrlToolHandler, registerTools, } from './tools.js';
|
|
9
13
|
import { shutdownTransformWorkerPool } from './transform.js';
|
|
14
|
+
import { isObject } from './type-guards.js';
|
|
10
15
|
function getLocalIcons() {
|
|
11
16
|
try {
|
|
12
17
|
const iconPath = new URL('../assets/logo.svg', import.meta.url);
|
|
@@ -36,6 +41,15 @@ function createServerCapabilities() {
|
|
|
36
41
|
tools: { listChanged: true },
|
|
37
42
|
resources: { listChanged: true, subscribe: true },
|
|
38
43
|
logging: {},
|
|
44
|
+
tasks: {
|
|
45
|
+
list: {},
|
|
46
|
+
cancel: {},
|
|
47
|
+
requests: {
|
|
48
|
+
tools: {
|
|
49
|
+
call: {},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
39
53
|
};
|
|
40
54
|
}
|
|
41
55
|
function createServerInstructions(serverVersion) {
|
|
@@ -62,6 +76,150 @@ function registerInstructionsResource(server, instructions) {
|
|
|
62
76
|
],
|
|
63
77
|
}));
|
|
64
78
|
}
|
|
79
|
+
// Schemas based on methods strings
|
|
80
|
+
const TaskGetSchema = z.object({ method: z.literal('tasks/get') });
|
|
81
|
+
const TaskListSchema = z.object({ method: z.literal('tasks/list') });
|
|
82
|
+
const TaskCancelSchema = z.object({ method: z.literal('tasks/cancel') });
|
|
83
|
+
const TaskResultSchema = z.object({ method: z.literal('tasks/result') });
|
|
84
|
+
function registerTaskHandlers(server) {
|
|
85
|
+
server.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
86
|
+
const extendedParams = request
|
|
87
|
+
.params;
|
|
88
|
+
const taskOptions = extendedParams.task;
|
|
89
|
+
if (taskOptions) {
|
|
90
|
+
// Validate tool support
|
|
91
|
+
if (extendedParams.name !== FETCH_URL_TOOL_NAME) {
|
|
92
|
+
throw new Error(`Tool '${extendedParams.name}' does not support task execution`);
|
|
93
|
+
}
|
|
94
|
+
// Create Task
|
|
95
|
+
const task = taskManager.createTask(taskOptions.ttl !== undefined ? { ttl: taskOptions.ttl } : undefined);
|
|
96
|
+
// Start Async Execution
|
|
97
|
+
void (async () => {
|
|
98
|
+
try {
|
|
99
|
+
const args = extendedParams.arguments;
|
|
100
|
+
if (!isObject(args) ||
|
|
101
|
+
typeof args.url !== 'string') {
|
|
102
|
+
throw new Error('Invalid arguments for fetch-url');
|
|
103
|
+
}
|
|
104
|
+
const validArgs = args;
|
|
105
|
+
const controller = new AbortController();
|
|
106
|
+
const result = await fetchUrlToolHandler(validArgs, {
|
|
107
|
+
signal: controller.signal,
|
|
108
|
+
requestId: task.taskId, // Correlation
|
|
109
|
+
...(extendedParams._meta ? { _meta: extendedParams._meta } : {}),
|
|
110
|
+
});
|
|
111
|
+
// Update Task on Success
|
|
112
|
+
taskManager.updateTask(task.taskId, {
|
|
113
|
+
status: 'completed',
|
|
114
|
+
result,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
// Update Task on Failure
|
|
119
|
+
taskManager.updateTask(task.taskId, {
|
|
120
|
+
status: 'failed',
|
|
121
|
+
statusMessage: error instanceof Error ? error.message : String(error),
|
|
122
|
+
error: error instanceof Error ? error.message : String(error),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
})();
|
|
126
|
+
// Return Immediate CreateTaskResult
|
|
127
|
+
const response = {
|
|
128
|
+
task: {
|
|
129
|
+
taskId: task.taskId,
|
|
130
|
+
status: task.status,
|
|
131
|
+
...(task.statusMessage ? { statusMessage: task.statusMessage } : {}),
|
|
132
|
+
createdAt: task.createdAt,
|
|
133
|
+
lastUpdatedAt: task.lastUpdatedAt,
|
|
134
|
+
ttl: task.ttl,
|
|
135
|
+
pollInterval: task.pollInterval,
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
return response;
|
|
139
|
+
}
|
|
140
|
+
if (extendedParams.name === FETCH_URL_TOOL_NAME) {
|
|
141
|
+
const args = extendedParams.arguments;
|
|
142
|
+
if (!isObject(args) ||
|
|
143
|
+
typeof args.url !== 'string') {
|
|
144
|
+
throw new Error('Invalid arguments for fetch-url');
|
|
145
|
+
}
|
|
146
|
+
return fetchUrlToolHandler({ url: args.url }, {
|
|
147
|
+
...(extendedParams._meta ? { _meta: extendedParams._meta } : {}),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
throw new Error(`Tool not found: ${extendedParams.name}`);
|
|
151
|
+
});
|
|
152
|
+
server.server.setRequestHandler(TaskGetSchema, async (request) => {
|
|
153
|
+
const { taskId } = request.params;
|
|
154
|
+
const task = taskManager.getTask(taskId);
|
|
155
|
+
if (!task) {
|
|
156
|
+
throw new Error('Task not found');
|
|
157
|
+
}
|
|
158
|
+
return Promise.resolve({
|
|
159
|
+
taskId: task.taskId,
|
|
160
|
+
status: task.status,
|
|
161
|
+
statusMessage: task.statusMessage,
|
|
162
|
+
createdAt: task.createdAt,
|
|
163
|
+
lastUpdatedAt: task.lastUpdatedAt,
|
|
164
|
+
ttl: task.ttl,
|
|
165
|
+
pollInterval: task.pollInterval,
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
server.server.setRequestHandler(TaskResultSchema, async (request) => {
|
|
169
|
+
const { taskId } = request.params;
|
|
170
|
+
const task = taskManager.getTask(taskId);
|
|
171
|
+
if (!task) {
|
|
172
|
+
throw new Error('Task not found');
|
|
173
|
+
}
|
|
174
|
+
if (task.status === 'working' || task.status === 'input_required') {
|
|
175
|
+
throw new Error('Task execution in progress');
|
|
176
|
+
}
|
|
177
|
+
if (task.status === 'failed') {
|
|
178
|
+
return Promise.resolve(task.result ?? { isError: true, content: [] });
|
|
179
|
+
}
|
|
180
|
+
if (task.status === 'cancelled') {
|
|
181
|
+
throw new Error('Task was cancelled');
|
|
182
|
+
}
|
|
183
|
+
const result = (task.result ?? { content: [] });
|
|
184
|
+
return Promise.resolve({
|
|
185
|
+
...result,
|
|
186
|
+
_meta: {
|
|
187
|
+
...result._meta,
|
|
188
|
+
'io.modelcontextprotocol/related-task': { taskId: task.taskId },
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
server.server.setRequestHandler(TaskListSchema, async () => {
|
|
193
|
+
const tasks = taskManager.listTasks();
|
|
194
|
+
return Promise.resolve({
|
|
195
|
+
tasks: tasks.map((t) => ({
|
|
196
|
+
taskId: t.taskId,
|
|
197
|
+
status: t.status,
|
|
198
|
+
createdAt: t.createdAt,
|
|
199
|
+
lastUpdatedAt: t.lastUpdatedAt,
|
|
200
|
+
ttl: t.ttl,
|
|
201
|
+
pollInterval: t.pollInterval,
|
|
202
|
+
})),
|
|
203
|
+
nextCursor: undefined,
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
server.server.setRequestHandler(TaskCancelSchema, async (request) => {
|
|
207
|
+
const { taskId } = request.params;
|
|
208
|
+
const task = taskManager.cancelTask(taskId);
|
|
209
|
+
if (!task) {
|
|
210
|
+
throw new Error('Task not found');
|
|
211
|
+
}
|
|
212
|
+
return Promise.resolve({
|
|
213
|
+
taskId: task.taskId,
|
|
214
|
+
status: task.status,
|
|
215
|
+
statusMessage: task.statusMessage,
|
|
216
|
+
createdAt: task.createdAt,
|
|
217
|
+
lastUpdatedAt: task.lastUpdatedAt,
|
|
218
|
+
ttl: task.ttl,
|
|
219
|
+
pollInterval: task.pollInterval,
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
}
|
|
65
223
|
export function createMcpServer() {
|
|
66
224
|
const instructions = createServerInstructions(config.server.version);
|
|
67
225
|
const server = new McpServer(createServerInfo(), {
|
|
@@ -73,6 +231,8 @@ export function createMcpServer() {
|
|
|
73
231
|
registerTools(server, localIcons);
|
|
74
232
|
registerCachedContentResource(server, localIcons);
|
|
75
233
|
registerInstructionsResource(server, instructions);
|
|
234
|
+
registerConfigResource(server);
|
|
235
|
+
registerTaskHandlers(server);
|
|
76
236
|
return server;
|
|
77
237
|
}
|
|
78
238
|
function attachServerErrorHandler(server) {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { config } from './config.js';
|
|
3
|
+
import { stableStringify } from './json.js';
|
|
4
|
+
/* -------------------------------------------------------------------------------------------------
|
|
5
|
+
* Configuration Resource
|
|
6
|
+
* ------------------------------------------------------------------------------------------------- */
|
|
7
|
+
function scrubAuth(auth) {
|
|
8
|
+
return {
|
|
9
|
+
...auth,
|
|
10
|
+
clientSecret: auth.clientSecret ? '<REDACTED>' : undefined,
|
|
11
|
+
staticTokens: auth.staticTokens.map(() => '<REDACTED>'),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function scrubSecurity(security) {
|
|
15
|
+
return {
|
|
16
|
+
...security,
|
|
17
|
+
apiKey: security.apiKey ? '<REDACTED>' : undefined,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function scrubConfig(source) {
|
|
21
|
+
return {
|
|
22
|
+
...source,
|
|
23
|
+
auth: scrubAuth(source.auth),
|
|
24
|
+
security: scrubSecurity(source.security),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function registerConfigResource(server) {
|
|
28
|
+
server.registerResource('config', new ResourceTemplate('internal://config', { list: undefined }), {
|
|
29
|
+
title: 'Server Configuration',
|
|
30
|
+
description: 'Current runtime configuration (secrets redacted)',
|
|
31
|
+
mimeType: 'application/json',
|
|
32
|
+
}, (uri) => {
|
|
33
|
+
const scrubbed = scrubConfig(config);
|
|
34
|
+
return {
|
|
35
|
+
contents: [
|
|
36
|
+
{
|
|
37
|
+
uri: uri.href,
|
|
38
|
+
mimeType: 'application/json',
|
|
39
|
+
text: stableStringify(scrubbed),
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
}
|