@j0hanz/superfetch 2.4.5 → 2.4.6

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/mcp.js CHANGED
@@ -1,8 +1,8 @@
1
- import { readFileSync } from 'node:fs';
1
+ import { readFile } from 'node:fs/promises';
2
2
  import { z } from 'zod';
3
3
  import { McpServer, ResourceTemplate, } from '@modelcontextprotocol/sdk/server/mcp.js';
4
4
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
- import { CallToolRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
5
+ import { CallToolRequestSchema, ErrorCode, McpError, } from '@modelcontextprotocol/sdk/types.js';
6
6
  import { registerCachedContentResource } from './cache.js';
7
7
  import { config } from './config.js';
8
8
  import { destroyAgents } from './fetch.js';
@@ -12,10 +12,10 @@ import { taskManager } from './tasks.js';
12
12
  import { FETCH_URL_TOOL_NAME, fetchUrlToolHandler, registerTools, } from './tools.js';
13
13
  import { shutdownTransformWorkerPool } from './transform.js';
14
14
  import { isObject } from './type-guards.js';
15
- function getLocalIcons() {
15
+ async function getLocalIcons() {
16
16
  try {
17
17
  const iconPath = new URL('../assets/logo.svg', import.meta.url);
18
- const buffer = readFileSync(iconPath);
18
+ const buffer = await readFile(iconPath);
19
19
  return [
20
20
  {
21
21
  src: `data:image/svg+xml;base64,${buffer.toString('base64')}`,
@@ -28,8 +28,8 @@ function getLocalIcons() {
28
28
  return undefined;
29
29
  }
30
30
  }
31
- function createServerInfo() {
32
- const localIcons = getLocalIcons();
31
+ async function createServerInfo() {
32
+ const localIcons = await getLocalIcons();
33
33
  return {
34
34
  name: config.server.name,
35
35
  version: config.server.version,
@@ -40,6 +40,7 @@ function createServerCapabilities() {
40
40
  return {
41
41
  tools: { listChanged: true },
42
42
  resources: { listChanged: true, subscribe: true },
43
+ prompts: {},
43
44
  logging: {},
44
45
  tasks: {
45
46
  list: {},
@@ -52,9 +53,9 @@ function createServerCapabilities() {
52
53
  },
53
54
  };
54
55
  }
55
- function createServerInstructions(serverVersion) {
56
+ async function createServerInstructions(serverVersion) {
56
57
  try {
57
- const raw = readFileSync(new URL('./instructions.md', import.meta.url), 'utf8');
58
+ const raw = await readFile(new URL('./instructions.md', import.meta.url), 'utf8');
58
59
  return raw.replaceAll('{{SERVER_VERSION}}', serverVersion).trim();
59
60
  }
60
61
  catch {
@@ -77,83 +78,233 @@ function registerInstructionsResource(server, instructions) {
77
78
  }));
78
79
  }
79
80
  // 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
- });
81
+ const TaskGetSchema = z.object({
82
+ method: z.literal('tasks/get'),
83
+ params: z.object({ taskId: z.string() }),
84
+ });
85
+ const TaskListSchema = z.object({
86
+ method: z.literal('tasks/list'),
87
+ params: z
88
+ .object({
89
+ cursor: z.string().optional(),
90
+ })
91
+ .optional(),
92
+ });
93
+ const TaskCancelSchema = z.object({
94
+ method: z.literal('tasks/cancel'),
95
+ params: z.object({ taskId: z.string() }),
96
+ });
97
+ const TaskResultSchema = z.object({
98
+ method: z.literal('tasks/result'),
99
+ params: z.object({ taskId: z.string() }),
100
+ });
101
+ function isNonEmptyString(value) {
102
+ return typeof value === 'string' && value.length > 0;
103
+ }
104
+ function isRecord(value) {
105
+ return isObject(value);
106
+ }
107
+ function isValidTask(task) {
108
+ if (task === undefined)
109
+ return true;
110
+ if (!isRecord(task))
111
+ return false;
112
+ const { ttl } = task;
113
+ return ttl === undefined || typeof ttl === 'number';
114
+ }
115
+ function isValidMeta(meta) {
116
+ if (meta === undefined)
117
+ return true;
118
+ if (!isRecord(meta))
119
+ return false;
120
+ const { progressToken } = meta;
121
+ if (progressToken !== undefined &&
122
+ typeof progressToken !== 'string' &&
123
+ typeof progressToken !== 'number') {
124
+ return false;
125
+ }
126
+ const related = meta['io.modelcontextprotocol/related-task'];
127
+ if (related === undefined)
128
+ return true;
129
+ if (!isRecord(related))
130
+ return false;
131
+ const { taskId } = related;
132
+ return typeof taskId === 'string';
133
+ }
134
+ function isExtendedCallToolRequest(request) {
135
+ if (!isRecord(request))
136
+ return false;
137
+ const { method, params } = request;
138
+ if (method !== 'tools/call')
139
+ return false;
140
+ if (!isRecord(params))
141
+ return false;
142
+ const { name, arguments: args, task, _meta, } = params;
143
+ return (isNonEmptyString(name) &&
144
+ (args === undefined || isRecord(args)) &&
145
+ isValidTask(task) &&
146
+ isValidMeta(_meta));
147
+ }
148
+ function resolveTaskOwnerKey(extra) {
149
+ if (extra?.sessionId)
150
+ return `session:${extra.sessionId}`;
151
+ if (extra?.authInfo?.clientId)
152
+ return `client:${extra.authInfo.clientId}`;
153
+ if (extra?.authInfo?.token)
154
+ return `token:${extra.authInfo.token}`;
155
+ return 'default';
156
+ }
157
+ function resolveToolCallContext(extra) {
158
+ const context = {
159
+ ownerKey: resolveTaskOwnerKey(extra),
160
+ };
161
+ if (extra?.signal)
162
+ context.signal = extra.signal;
163
+ if (extra?.requestId !== undefined)
164
+ context.requestId = extra.requestId;
165
+ if (extra?.sendNotification)
166
+ context.sendNotification = extra.sendNotification;
167
+ return context;
168
+ }
169
+ function requireFetchUrlArgs(args) {
170
+ if (!isObject(args) || typeof args.url !== 'string') {
171
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid arguments for fetch-url');
172
+ }
173
+ return { url: args.url };
174
+ }
175
+ function throwTaskNotFound() {
176
+ throw new McpError(ErrorCode.InvalidParams, 'Failed to retrieve task: Task not found');
177
+ }
178
+ function requireFetchUrlToolName(name) {
179
+ if (name === FETCH_URL_TOOL_NAME)
180
+ return;
181
+ throw new McpError(ErrorCode.MethodNotFound, `Tool '${name}' does not support task execution`);
182
+ }
183
+ function buildRelatedTaskMeta(taskId, meta) {
184
+ return {
185
+ ...(meta ?? {}),
186
+ 'io.modelcontextprotocol/related-task': { taskId },
187
+ };
188
+ }
189
+ function buildCreateTaskResult(task) {
190
+ return {
191
+ task,
192
+ _meta: {
193
+ 'io.modelcontextprotocol/related-task': {
194
+ taskId: task.taskId,
195
+ status: task.status,
196
+ ...(task.statusMessage ? { statusMessage: task.statusMessage } : {}),
197
+ createdAt: task.createdAt,
198
+ lastUpdatedAt: task.lastUpdatedAt,
199
+ ttl: task.ttl,
200
+ pollInterval: task.pollInterval,
201
+ },
202
+ },
203
+ };
204
+ }
205
+ async function runFetchTaskExecution(params) {
206
+ const { taskId, args, meta, sendNotification } = params;
207
+ try {
208
+ const controller = new AbortController();
209
+ const relatedMeta = buildRelatedTaskMeta(taskId, meta);
210
+ const result = await fetchUrlToolHandler(args, {
211
+ signal: controller.signal,
212
+ requestId: taskId, // Correlation
213
+ _meta: relatedMeta,
214
+ ...(sendNotification ? { sendNotification } : {}),
215
+ });
216
+ const isToolError = typeof result.isError === 'boolean'
217
+ ? result.isError
218
+ : false;
219
+ taskManager.updateTask(taskId, {
220
+ status: isToolError ? 'failed' : 'completed',
221
+ ...(isToolError
222
+ ? {
223
+ statusMessage: result
224
+ .structuredContent?.error ?? 'Tool execution failed',
124
225
  }
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');
226
+ : {}),
227
+ result,
228
+ });
229
+ }
230
+ catch (error) {
231
+ const errorMessage = error instanceof Error ? error.message : String(error);
232
+ const errorPayload = error instanceof McpError
233
+ ? {
234
+ code: error.code,
235
+ message: errorMessage,
236
+ data: error.data,
145
237
  }
146
- return fetchUrlToolHandler({ url: args.url }, {
147
- ...(extendedParams._meta ? { _meta: extendedParams._meta } : {}),
148
- });
238
+ : {
239
+ code: ErrorCode.InternalError,
240
+ message: errorMessage,
241
+ };
242
+ taskManager.updateTask(taskId, {
243
+ status: 'failed',
244
+ statusMessage: errorMessage,
245
+ error: errorPayload,
246
+ });
247
+ }
248
+ }
249
+ function handleTaskToolCall(params, context) {
250
+ requireFetchUrlToolName(params.name);
251
+ const validArgs = requireFetchUrlArgs(params.arguments);
252
+ const task = taskManager.createTask(params.task?.ttl !== undefined ? { ttl: params.task.ttl } : undefined, 'Task started', context.ownerKey);
253
+ const executionParams = {
254
+ taskId: task.taskId,
255
+ args: validArgs,
256
+ ...(params._meta ? { meta: params._meta } : {}),
257
+ ...(context.sendNotification
258
+ ? { sendNotification: context.sendNotification }
259
+ : {}),
260
+ };
261
+ void runFetchTaskExecution(executionParams);
262
+ return buildCreateTaskResult({
263
+ taskId: task.taskId,
264
+ status: task.status,
265
+ ...(task.statusMessage ? { statusMessage: task.statusMessage } : {}),
266
+ createdAt: task.createdAt,
267
+ lastUpdatedAt: task.lastUpdatedAt,
268
+ ttl: task.ttl,
269
+ pollInterval: task.pollInterval,
270
+ });
271
+ }
272
+ async function handleDirectToolCall(params, context) {
273
+ const args = requireFetchUrlArgs(params.arguments);
274
+ return fetchUrlToolHandler({ url: args.url }, {
275
+ ...(context.signal ? { signal: context.signal } : {}),
276
+ ...(context.requestId ? { requestId: context.requestId } : {}),
277
+ ...(context.sendNotification
278
+ ? { sendNotification: context.sendNotification }
279
+ : {}),
280
+ ...(params._meta ? { _meta: params._meta } : {}),
281
+ });
282
+ }
283
+ async function handleToolCallRequest(request, context) {
284
+ const { params } = request;
285
+ if (params.task) {
286
+ return handleTaskToolCall(params, context);
287
+ }
288
+ if (params.name === FETCH_URL_TOOL_NAME) {
289
+ return handleDirectToolCall(params, context);
290
+ }
291
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${params.name}`);
292
+ }
293
+ function registerTaskHandlers(server) {
294
+ server.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
295
+ const context = resolveToolCallContext(extra);
296
+ if (!isExtendedCallToolRequest(request)) {
297
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid tool request');
149
298
  }
150
- throw new Error(`Tool not found: ${extendedParams.name}`);
299
+ const result = await handleToolCallRequest(request, context);
300
+ return result;
151
301
  });
152
- server.server.setRequestHandler(TaskGetSchema, async (request) => {
302
+ server.server.setRequestHandler(TaskGetSchema, async (request, extra) => {
153
303
  const { taskId } = request.params;
154
- const task = taskManager.getTask(taskId);
304
+ const ownerKey = resolveTaskOwnerKey(extra);
305
+ const task = taskManager.getTask(taskId, ownerKey);
155
306
  if (!task) {
156
- throw new Error('Task not found');
307
+ throwTaskNotFound();
157
308
  }
158
309
  return Promise.resolve({
159
310
  taskId: task.taskId,
@@ -165,20 +316,37 @@ function registerTaskHandlers(server) {
165
316
  pollInterval: task.pollInterval,
166
317
  });
167
318
  });
168
- server.server.setRequestHandler(TaskResultSchema, async (request) => {
319
+ server.server.setRequestHandler(TaskResultSchema, async (request, extra) => {
169
320
  const { taskId } = request.params;
170
- const task = taskManager.getTask(taskId);
321
+ const ownerKey = resolveTaskOwnerKey(extra);
322
+ const task = await taskManager.waitForTerminalTask(taskId, ownerKey, extra?.signal);
171
323
  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');
324
+ throwTaskNotFound();
176
325
  }
177
326
  if (task.status === 'failed') {
178
- return Promise.resolve(task.result ?? { isError: true, content: [] });
327
+ if (task.error) {
328
+ throw new McpError(task.error.code, task.error.message, task.error.data);
329
+ }
330
+ const failedResult = (task.result ?? null);
331
+ const fallback = failedResult ?? {
332
+ content: [
333
+ {
334
+ type: 'text',
335
+ text: task.statusMessage ?? 'Task execution failed',
336
+ },
337
+ ],
338
+ isError: true,
339
+ };
340
+ return Promise.resolve({
341
+ ...fallback,
342
+ _meta: {
343
+ ...fallback._meta,
344
+ 'io.modelcontextprotocol/related-task': { taskId: task.taskId },
345
+ },
346
+ });
179
347
  }
180
348
  if (task.status === 'cancelled') {
181
- throw new Error('Task was cancelled');
349
+ throw new McpError(ErrorCode.InvalidRequest, 'Task was cancelled');
182
350
  }
183
351
  const result = (task.result ?? { content: [] });
184
352
  return Promise.resolve({
@@ -189,8 +357,10 @@ function registerTaskHandlers(server) {
189
357
  },
190
358
  });
191
359
  });
192
- server.server.setRequestHandler(TaskListSchema, async () => {
193
- const tasks = taskManager.listTasks();
360
+ server.server.setRequestHandler(TaskListSchema, async (request, extra) => {
361
+ const ownerKey = resolveTaskOwnerKey(extra);
362
+ const cursor = request.params?.cursor;
363
+ const { tasks, nextCursor } = taskManager.listTasks(cursor === undefined ? { ownerKey } : { ownerKey, cursor });
194
364
  return Promise.resolve({
195
365
  tasks: tasks.map((t) => ({
196
366
  taskId: t.taskId,
@@ -200,14 +370,15 @@ function registerTaskHandlers(server) {
200
370
  ttl: t.ttl,
201
371
  pollInterval: t.pollInterval,
202
372
  })),
203
- nextCursor: undefined,
373
+ nextCursor,
204
374
  });
205
375
  });
206
- server.server.setRequestHandler(TaskCancelSchema, async (request) => {
376
+ server.server.setRequestHandler(TaskCancelSchema, async (request, extra) => {
207
377
  const { taskId } = request.params;
208
- const task = taskManager.cancelTask(taskId);
378
+ const ownerKey = resolveTaskOwnerKey(extra);
379
+ const task = taskManager.cancelTask(taskId, ownerKey);
209
380
  if (!task) {
210
- throw new Error('Task not found');
381
+ throwTaskNotFound();
211
382
  }
212
383
  return Promise.resolve({
213
384
  taskId: task.taskId,
@@ -240,14 +411,15 @@ function registerPrompts(server) {
240
411
  }));
241
412
  }
242
413
  }
243
- export function createMcpServer() {
244
- const instructions = createServerInstructions(config.server.version);
245
- const server = new McpServer(createServerInfo(), {
414
+ export async function createMcpServer() {
415
+ const instructions = await createServerInstructions(config.server.version);
416
+ const serverInfo = await createServerInfo();
417
+ const server = new McpServer(serverInfo, {
246
418
  capabilities: createServerCapabilities(),
247
419
  instructions,
248
420
  });
249
421
  setMcpServer(server);
250
- const localIcons = getLocalIcons();
422
+ const localIcons = await getLocalIcons();
251
423
  registerTools(server);
252
424
  registerCachedContentResource(server, localIcons);
253
425
  registerInstructionsResource(server, instructions);
@@ -311,7 +483,7 @@ async function connectStdioServer(server, transport) {
311
483
  }
312
484
  }
313
485
  export async function startStdioServer() {
314
- const server = createMcpServer();
486
+ const server = await createMcpServer();
315
487
  const transport = new StdioServerTransport();
316
488
  attachServerErrorHandler(server);
317
489
  registerSignalHandlers(createShutdownHandler(server));
@@ -65,11 +65,12 @@ function writeLog(level, message, meta) {
65
65
  return;
66
66
  process.stderr.write(`${formatLogEntry(level, message, meta)}\n`);
67
67
  if (mcpServer) {
68
+ const sessionId = getSessionId();
68
69
  mcpServer.server
69
70
  .sendLoggingMessage({
70
71
  level: mapToMcpLevel(level),
71
72
  data: meta ? { message, ...meta } : message,
72
- })
73
+ }, sessionId)
73
74
  .catch(() => { });
74
75
  }
75
76
  }
package/dist/tasks.d.ts CHANGED
@@ -1,6 +1,12 @@
1
1
  export type TaskStatus = 'working' | 'input_required' | 'completed' | 'failed' | 'cancelled';
2
+ export interface TaskError {
3
+ code: number;
4
+ message: string;
5
+ data?: unknown;
6
+ }
2
7
  export interface TaskState {
3
8
  taskId: string;
9
+ ownerKey: string;
4
10
  status: TaskStatus;
5
11
  statusMessage?: string;
6
12
  createdAt: string;
@@ -8,7 +14,7 @@ export interface TaskState {
8
14
  ttl: number;
9
15
  pollInterval: number;
10
16
  result?: unknown;
11
- error?: unknown;
17
+ error?: TaskError;
12
18
  }
13
19
  export interface CreateTaskOptions {
14
20
  ttl?: number;
@@ -27,11 +33,24 @@ export interface CreateTaskResult {
27
33
  }
28
34
  export declare class TaskManager {
29
35
  private tasks;
30
- createTask(options?: CreateTaskOptions, statusMessage?: string): TaskState;
31
- getTask(taskId: string): TaskState | undefined;
36
+ private waiters;
37
+ createTask(options?: CreateTaskOptions, statusMessage?: string, ownerKey?: string): TaskState;
38
+ getTask(taskId: string, ownerKey?: string): TaskState | undefined;
32
39
  updateTask(taskId: string, updates: Partial<Omit<TaskState, 'taskId' | 'createdAt'>>): void;
33
- cancelTask(taskId: string): TaskState | undefined;
34
- listTasks(): TaskState[];
40
+ cancelTask(taskId: string, ownerKey?: string): TaskState | undefined;
41
+ listTasks(options: {
42
+ ownerKey: string;
43
+ cursor?: string;
44
+ limit?: number;
45
+ }): {
46
+ tasks: TaskState[];
47
+ nextCursor?: string;
48
+ };
35
49
  cleanupExpiredTasks(): number;
50
+ waitForTerminalTask(taskId: string, ownerKey: string, signal?: AbortSignal): Promise<TaskState | undefined>;
51
+ private notifyWaiters;
52
+ private isExpired;
53
+ private encodeCursor;
54
+ private decodeCursor;
36
55
  }
37
56
  export declare const taskManager: TaskManager;