@j0hanz/superfetch 2.4.2 → 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.
@@ -1,7 +1,7 @@
1
1
  import { config } from './config.js';
2
- // ─────────────────────────────────────────────────────────────────────────────
3
- // Fence state helpers
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
- // Segment utilities
34
- // ─────────────────────────────────────────────────────────────────────────────
35
- /**
36
- * Split markdown into segments where each segment is either fully inside
37
- * a fenced block (including the fence lines), or fully outside.
38
- */
39
- function splitByFences(content) {
40
- const lines = content.split('\n');
41
- const segments = [];
42
- const state = initialFenceState();
43
- let current = [];
44
- let currentIsFence = false;
45
- for (const line of lines) {
46
- // Transition into fence: flush outside segment first.
47
- if (!state.inFence && isFenceStart(line)) {
48
- if (current.length > 0) {
49
- segments.push({ content: current.join('\n'), inFence: currentIsFence });
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
- continue;
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.push(line);
58
- const wasInFence = state.inFence;
59
- advanceFenceState(line, state);
60
- // Transition out of fence: flush fence segment.
61
- if (wasInFence && !state.inFence) {
62
- segments.push({ content: current.join('\n'), inFence: true });
63
- current = [];
64
- currentIsFence = false;
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
- if (current.length > 0) {
68
- segments.push({ content: current.join('\n'), inFence: currentIsFence });
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
- // Cleanup rules (OUTSIDE fences only)
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)*\]\(#[^)]*\)\s*/g;
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
- if (!content)
186
- return '';
187
- const segments = splitByFences(content);
188
- return segments
189
- .map((seg, index) => {
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
- function findFrontmatterLines(content) {
235
- const lineEnding = detectLineEnding(content);
236
- const lines = content.split(lineEnding);
237
- if (lines[0] !== FRONTMATTER_DELIMITER)
238
- return null;
239
- const endIndex = lines.indexOf(FRONTMATTER_DELIMITER, 1);
240
- if (endIndex === -1)
241
- return null;
242
- return { lineEnding, lines, endIndex };
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 frontmatter = findFrontmatterLines(content);
292
- if (!frontmatter) {
373
+ const fm = frontmatter.find(content);
374
+ if (!fm) {
293
375
  return extractTitleFromHeading(content);
294
376
  }
295
- const { lines, endIndex } = frontmatter;
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 frontmatter = findFrontmatterLines(content);
343
- if (config.transform.metadataFormat === 'markdown' && !frontmatter) {
424
+ const fm = frontmatter.find(content);
425
+ if (config.transform.metadataFormat === 'markdown' && !fm) {
344
426
  return addSourceToMarkdownMarkdownFormat(content, url);
345
427
  }
346
- if (!frontmatter) {
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 } = frontmatter;
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
- // Heading Promotion (fence-aware)
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(processNonFencedLine(line, prevLine));
527
+ result.push(orphanHeadingPromoter.processLine(line, prevLine));
515
528
  }
516
529
  return result.join('\n');
517
530
  }
package/dist/mcp.js CHANGED
@@ -1,34 +1,39 @@
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 { registerTools } from './tools.js';
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';
10
- function getLocalIconData() {
14
+ import { isObject } from './type-guards.js';
15
+ function getLocalIcons() {
11
16
  try {
12
17
  const iconPath = new URL('../assets/logo.svg', import.meta.url);
13
18
  const buffer = readFileSync(iconPath);
14
- return `data:image/svg+xml;base64,${buffer.toString('base64')}`;
19
+ return [
20
+ {
21
+ src: `data:image/svg+xml;base64,${buffer.toString('base64')}`,
22
+ mimeType: 'image/svg+xml',
23
+ sizes: ['any'],
24
+ },
25
+ ];
15
26
  }
16
27
  catch {
17
28
  return undefined;
18
29
  }
19
30
  }
20
31
  function createServerInfo() {
21
- const localIcon = getLocalIconData();
32
+ const localIcons = getLocalIcons();
22
33
  return {
23
34
  name: config.server.name,
24
35
  version: config.server.version,
25
- ...(localIcon
26
- ? {
27
- icons: [
28
- { src: localIcon, mimeType: 'image/svg+xml', sizes: ['any'] },
29
- ],
30
- }
31
- : {}),
36
+ ...(localIcons ? { icons: localIcons } : {}),
32
37
  };
33
38
  }
34
39
  function createServerCapabilities() {
@@ -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(), {
@@ -69,10 +227,12 @@ export function createMcpServer() {
69
227
  instructions,
70
228
  });
71
229
  setMcpServer(server);
72
- const localIcon = getLocalIconData();
73
- registerTools(server, localIcon);
74
- registerCachedContentResource(server, localIcon);
230
+ const localIcons = getLocalIcons();
231
+ registerTools(server, localIcons);
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,2 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerConfigResource(server: McpServer): void;