@j0hanz/fetch-url-mcp 1.12.7 → 1.12.9

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.
Files changed (147) hide show
  1. package/dist/http/auth.d.ts +9 -4
  2. package/dist/http/auth.d.ts.map +1 -1
  3. package/dist/http/auth.js +30 -5
  4. package/dist/http/index.d.ts +4 -0
  5. package/dist/http/index.d.ts.map +1 -0
  6. package/dist/http/index.js +5 -0
  7. package/dist/http/native.d.ts +73 -0
  8. package/dist/http/native.d.ts.map +1 -1
  9. package/dist/http/native.js +597 -10
  10. package/dist/http/rate-limit.d.ts +7 -2
  11. package/dist/http/rate-limit.d.ts.map +1 -1
  12. package/dist/http/rate-limit.js +9 -4
  13. package/dist/index.d.ts +17 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +68 -6
  16. package/dist/lib/config.d.ts.map +1 -1
  17. package/dist/lib/config.js +22 -2
  18. package/dist/lib/core.d.ts +57 -5
  19. package/dist/lib/core.d.ts.map +1 -1
  20. package/dist/lib/core.js +155 -5
  21. package/dist/lib/error/classes.d.ts +19 -0
  22. package/dist/lib/error/classes.d.ts.map +1 -0
  23. package/dist/lib/error/classes.js +107 -0
  24. package/dist/lib/error/classify.d.ts +4 -0
  25. package/dist/lib/error/classify.d.ts.map +1 -0
  26. package/dist/lib/error/classify.js +154 -0
  27. package/dist/lib/error/codes.d.ts +23 -0
  28. package/dist/lib/error/codes.d.ts.map +1 -0
  29. package/dist/lib/error/codes.js +22 -0
  30. package/dist/lib/error/index.d.ts +6 -0
  31. package/dist/lib/error/index.d.ts.map +1 -0
  32. package/dist/lib/error/index.js +7 -0
  33. package/dist/lib/{error-messages.d.ts → error/messages.d.ts} +2 -2
  34. package/dist/lib/error/messages.d.ts.map +1 -0
  35. package/dist/lib/{error-messages.js → error/messages.js} +2 -2
  36. package/dist/lib/{tool-errors.d.ts → error/payload.d.ts} +7 -13
  37. package/dist/lib/error/payload.d.ts.map +1 -0
  38. package/dist/lib/error/payload.js +108 -0
  39. package/dist/lib/mcp-interop.d.ts.map +1 -1
  40. package/dist/lib/mcp-interop.js +4 -6
  41. package/dist/lib/net/http.d.ts.map +1 -0
  42. package/dist/lib/{http.js → net/http.js} +5 -7
  43. package/dist/lib/net/index.d.ts +4 -0
  44. package/dist/lib/net/index.d.ts.map +1 -0
  45. package/dist/lib/net/index.js +5 -0
  46. package/dist/lib/{fetch-pipeline.d.ts → net/pipeline.d.ts} +3 -3
  47. package/dist/lib/net/pipeline.d.ts.map +1 -0
  48. package/dist/lib/{fetch-pipeline.js → net/pipeline.js} +4 -5
  49. package/dist/lib/{url.d.ts → net/url.d.ts} +1 -1
  50. package/dist/lib/net/url.d.ts.map +1 -0
  51. package/dist/lib/{url.js → net/url.js} +4 -5
  52. package/dist/lib/utils.d.ts +2 -32
  53. package/dist/lib/utils.d.ts.map +1 -1
  54. package/dist/lib/utils.js +28 -146
  55. package/dist/resources/index.d.ts.map +1 -1
  56. package/dist/resources/index.js +11 -6
  57. package/dist/schemas.d.ts +1 -1
  58. package/dist/schemas.js +1 -1
  59. package/dist/server.d.ts.map +1 -1
  60. package/dist/server.js +8 -9
  61. package/dist/tasks/index.d.ts +2 -0
  62. package/dist/tasks/index.d.ts.map +1 -0
  63. package/dist/tasks/index.js +3 -0
  64. package/dist/tasks/manager.d.ts +123 -1
  65. package/dist/tasks/manager.d.ts.map +1 -1
  66. package/dist/tasks/manager.js +756 -10
  67. package/dist/tools/{fetch-url.d.ts → index.d.ts} +4 -5
  68. package/dist/tools/index.d.ts.map +1 -0
  69. package/dist/tools/{fetch-url.js → index.js} +7 -8
  70. package/dist/transform/index.d.ts +279 -0
  71. package/dist/transform/index.d.ts.map +1 -0
  72. package/dist/transform/index.js +5235 -0
  73. package/package.json +2 -2
  74. package/dist/cli.d.ts +0 -19
  75. package/dist/cli.d.ts.map +0 -1
  76. package/dist/cli.js +0 -65
  77. package/dist/http/health.d.ts +0 -8
  78. package/dist/http/health.d.ts.map +0 -1
  79. package/dist/http/health.js +0 -152
  80. package/dist/http/helpers.d.ts +0 -68
  81. package/dist/http/helpers.d.ts.map +0 -1
  82. package/dist/http/helpers.js +0 -402
  83. package/dist/lib/error-codes.d.ts +0 -13
  84. package/dist/lib/error-codes.d.ts.map +0 -1
  85. package/dist/lib/error-codes.js +0 -12
  86. package/dist/lib/error-messages.d.ts.map +0 -1
  87. package/dist/lib/fetch-pipeline.d.ts.map +0 -1
  88. package/dist/lib/http.d.ts.map +0 -1
  89. package/dist/lib/logger-names.d.ts +0 -16
  90. package/dist/lib/logger-names.d.ts.map +0 -1
  91. package/dist/lib/logger-names.js +0 -15
  92. package/dist/lib/session.d.ts +0 -44
  93. package/dist/lib/session.d.ts.map +0 -1
  94. package/dist/lib/session.js +0 -137
  95. package/dist/lib/tool-errors.d.ts.map +0 -1
  96. package/dist/lib/tool-errors.js +0 -253
  97. package/dist/lib/url.d.ts.map +0 -1
  98. package/dist/lib/zod.d.ts +0 -3
  99. package/dist/lib/zod.d.ts.map +0 -1
  100. package/dist/lib/zod.js +0 -27
  101. package/dist/tasks/call-contract.d.ts +0 -25
  102. package/dist/tasks/call-contract.d.ts.map +0 -1
  103. package/dist/tasks/call-contract.js +0 -59
  104. package/dist/tasks/execution.d.ts +0 -16
  105. package/dist/tasks/execution.d.ts.map +0 -1
  106. package/dist/tasks/execution.js +0 -241
  107. package/dist/tasks/handlers.d.ts +0 -11
  108. package/dist/tasks/handlers.d.ts.map +0 -1
  109. package/dist/tasks/handlers.js +0 -157
  110. package/dist/tasks/owner.d.ts +0 -43
  111. package/dist/tasks/owner.d.ts.map +0 -1
  112. package/dist/tasks/owner.js +0 -144
  113. package/dist/tasks/registry.d.ts +0 -20
  114. package/dist/tasks/registry.d.ts.map +0 -1
  115. package/dist/tasks/registry.js +0 -40
  116. package/dist/tasks/waiters.d.ts +0 -27
  117. package/dist/tasks/waiters.d.ts.map +0 -1
  118. package/dist/tasks/waiters.js +0 -114
  119. package/dist/tools/fetch-url.d.ts.map +0 -1
  120. package/dist/transform/dom-prep.d.ts +0 -16
  121. package/dist/transform/dom-prep.d.ts.map +0 -1
  122. package/dist/transform/dom-prep.js +0 -1287
  123. package/dist/transform/html-translators.d.ts +0 -5
  124. package/dist/transform/html-translators.d.ts.map +0 -1
  125. package/dist/transform/html-translators.js +0 -697
  126. package/dist/transform/markdown-cleanup.d.ts +0 -10
  127. package/dist/transform/markdown-cleanup.d.ts.map +0 -1
  128. package/dist/transform/markdown-cleanup.js +0 -542
  129. package/dist/transform/metadata.d.ts +0 -18
  130. package/dist/transform/metadata.d.ts.map +0 -1
  131. package/dist/transform/metadata.js +0 -462
  132. package/dist/transform/next-flight.d.ts +0 -2
  133. package/dist/transform/next-flight.d.ts.map +0 -1
  134. package/dist/transform/next-flight.js +0 -374
  135. package/dist/transform/shared.d.ts +0 -8
  136. package/dist/transform/shared.d.ts.map +0 -1
  137. package/dist/transform/shared.js +0 -137
  138. package/dist/transform/transform.d.ts +0 -38
  139. package/dist/transform/transform.d.ts.map +0 -1
  140. package/dist/transform/transform.js +0 -1042
  141. package/dist/transform/types.d.ts +0 -124
  142. package/dist/transform/types.d.ts.map +0 -1
  143. package/dist/transform/types.js +0 -5
  144. package/dist/transform/worker-pool.d.ts +0 -76
  145. package/dist/transform/worker-pool.d.ts.map +0 -1
  146. package/dist/transform/worker-pool.js +0 -725
  147. /package/dist/lib/{http.d.ts → net/http.d.ts} +0 -0
@@ -1,12 +1,470 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { createHmac, randomBytes } from 'node:crypto';
1
+ import {} from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { CallToolRequestSchema, ErrorCode, McpError, } from '@modelcontextprotocol/sdk/types.js';
3
+ import { AsyncLocalStorage } from 'node:async_hooks';
4
+ import { createHmac, hash, randomBytes, randomUUID } from 'node:crypto';
3
5
  import { setInterval } from 'node:timers';
4
- import { ErrorCode } from '@modelcontextprotocol/sdk/types.js';
5
- import { config, logInfo, logWarn } from '../lib/core.js';
6
- import { Loggers } from '../lib/logger-names.js';
7
- import { createMcpError } from '../lib/mcp-interop.js';
8
- import { isObject, timingSafeEqualUtf8 } from '../lib/utils.js';
9
- import { TaskWaiterRegistry, waitForTerminalTask as waitForTerminalTaskWithDeadline, } from './waiters.js';
6
+ import { z } from 'zod';
7
+ import { config } from '../lib/config.js';
8
+ import { getRequestId, logDebug, logError, Loggers, logInfo, logWarn, resolveMcpSessionOwnerKey, runWithRequestContext, } from '../lib/core.js';
9
+ import { getErrorMessage, handleToolError, stripMcpErrorPrefix, toError, tryReadToolErrorMessage, } from '../lib/error/index.js';
10
+ import { createMcpError, getSdkCallToolHandler, registerServerLifecycleCleanup, } from '../lib/mcp-interop.js';
11
+ import { createUnrefTimeout, formatZodError, isObject, timingSafeEqualUtf8, } from '../lib/utils.js';
12
+ /*
13
+ * Module map:
14
+ * - call-tool request parsing
15
+ * - Abort-controller management for in-flight task executions
16
+ * - Task notification and validation helpers
17
+ * - Execution pipeline
18
+ * - Task handler schemas and registration
19
+ * - Handler extra parsing & owner-key resolution
20
+ * Own task lifecycle and MCP task wiring here. Keep tool business logic and HTTP transport details elsewhere.
21
+ */
22
+ const MIN_TASK_TTL_MS = 1_000;
23
+ const MAX_TASK_TTL_MS = 86_400_000;
24
+ const relatedTaskMetaSchema = z.strictObject({
25
+ taskId: z.string(),
26
+ });
27
+ const toolCallMetaSchema = z.looseObject({
28
+ progressToken: z.union([z.string(), z.number()]).optional(),
29
+ 'io.modelcontextprotocol/related-task': relatedTaskMetaSchema.optional(),
30
+ });
31
+ export const extendedCallToolRequestSchema = z.looseObject({
32
+ method: z.literal('tools/call'),
33
+ params: z.strictObject({
34
+ name: z.string().min(1, 'Tool name required'),
35
+ arguments: z.record(z.string(), z.unknown()).optional(),
36
+ task: z
37
+ .strictObject({
38
+ ttl: z
39
+ .number()
40
+ .int()
41
+ .min(MIN_TASK_TTL_MS, `Minimum ${MIN_TASK_TTL_MS}ms`)
42
+ .max(MAX_TASK_TTL_MS, `Maximum ${MAX_TASK_TTL_MS}ms`)
43
+ .optional(),
44
+ })
45
+ .optional(),
46
+ _meta: toolCallMetaSchema.optional(),
47
+ }),
48
+ });
49
+ export function parseExtendedCallToolRequest(request) {
50
+ const parsed = extendedCallToolRequestSchema.safeParse(request);
51
+ if (parsed.success)
52
+ return parsed.data;
53
+ throw createMcpError(ErrorCode.InvalidParams, formatZodError(parsed.error));
54
+ }
55
+ export function sanitizeToolCallMeta(meta) {
56
+ if (!meta)
57
+ return undefined;
58
+ const sanitized = { ...meta };
59
+ delete sanitized['io.modelcontextprotocol/related-task'];
60
+ return Object.keys(sanitized).length > 0 ? sanitized : undefined;
61
+ }
62
+ export function buildRelatedTaskMeta(taskId, meta) {
63
+ return {
64
+ ...(sanitizeToolCallMeta(meta) ?? {}),
65
+ 'io.modelcontextprotocol/related-task': { taskId },
66
+ };
67
+ }
68
+ export function withRelatedTaskMeta(result, taskId) {
69
+ return {
70
+ ...result,
71
+ _meta: {
72
+ ...result._meta,
73
+ 'io.modelcontextprotocol/related-task': { taskId },
74
+ },
75
+ };
76
+ }
77
+ /* -------------------------------------------------------------------------------------------------
78
+ * Abort-controller management for in-flight task executions
79
+ * ------------------------------------------------------------------------------------------------- */
80
+ // Intentionally process-global (not session-scoped): abortAllTaskExecutions() is called
81
+ // during SIGTERM/SIGINT shutdown to cancel every in-flight task across all sessions.
82
+ const taskAbortControllers = new Map();
83
+ function attachAbortController(taskId) {
84
+ taskAbortControllers.get(taskId)?.abort();
85
+ if (taskAbortControllers.size >= config.tasks.maxTotal) {
86
+ logWarn('Abort controller map reached task capacity — possible leak', {
87
+ size: taskAbortControllers.size,
88
+ maxTotal: config.tasks.maxTotal,
89
+ }, Loggers.LOG_TASKS);
90
+ }
91
+ const controller = new AbortController();
92
+ taskAbortControllers.set(taskId, controller);
93
+ return controller;
94
+ }
95
+ export function abortTaskExecution(taskId) {
96
+ taskAbortControllers.get(taskId)?.abort();
97
+ taskAbortControllers.delete(taskId);
98
+ }
99
+ export function cancelTasksForOwner(ownerKey, statusMessage = 'The task was cancelled because its owner session ended.') {
100
+ if (!ownerKey)
101
+ return 0;
102
+ const cancelled = taskManager.cancelTasksByOwner(ownerKey, statusMessage);
103
+ for (const task of cancelled) {
104
+ abortTaskExecution(task.taskId);
105
+ }
106
+ return cancelled.length;
107
+ }
108
+ export function abortAllTaskExecutions() {
109
+ for (const taskId of taskAbortControllers.keys())
110
+ abortTaskExecution(taskId);
111
+ }
112
+ export function toTaskSummary(task) {
113
+ return {
114
+ taskId: task.taskId,
115
+ status: task.status,
116
+ ...(task.statusMessage ? { statusMessage: task.statusMessage } : {}),
117
+ ...(task.progress !== undefined ? { progress: task.progress } : {}),
118
+ ...(task.total !== undefined ? { total: task.total } : {}),
119
+ createdAt: task.createdAt,
120
+ lastUpdatedAt: task.lastUpdatedAt,
121
+ ttl: task.ttl,
122
+ pollInterval: task.pollInterval,
123
+ };
124
+ }
125
+ export function emitTaskStatusNotification(server, task) {
126
+ if (!config.tasks.emitStatusNotifications || !server.isConnected())
127
+ return;
128
+ void server.server
129
+ .notification({
130
+ method: 'notifications/tasks/status',
131
+ params: { ...toTaskSummary(task) },
132
+ })
133
+ .catch((error) => {
134
+ logError('Failed to send task status notification', {
135
+ taskId: task.taskId,
136
+ status: task.status,
137
+ error: getErrorMessage(error),
138
+ }, Loggers.LOG_TASKS);
139
+ });
140
+ }
141
+ export function throwTaskNotFound() {
142
+ throw createMcpError(ErrorCode.InvalidParams, 'Task not found');
143
+ }
144
+ /* -------------------------------------------------------------------------------------------------
145
+ * Execution pipeline
146
+ * ------------------------------------------------------------------------------------------------- */
147
+ function updateTaskAndEmitStatus(server, taskId, update) {
148
+ taskManager.updateTask(taskId, update);
149
+ const task = taskManager.getTask(taskId);
150
+ if (task)
151
+ emitTaskStatusNotification(server, task);
152
+ }
153
+ function buildTaskFailureState(error) {
154
+ const mcpErrorMessage = error instanceof McpError ? stripMcpErrorPrefix(error.message) : undefined;
155
+ const statusMessage = mcpErrorMessage ?? getErrorMessage(error);
156
+ if (error instanceof McpError) {
157
+ return {
158
+ status: 'failed',
159
+ statusMessage,
160
+ error: {
161
+ code: error.code,
162
+ ...(error.data !== undefined ? { data: error.data } : {}),
163
+ message: statusMessage,
164
+ },
165
+ };
166
+ }
167
+ return {
168
+ status: 'failed',
169
+ statusMessage,
170
+ error: {
171
+ code: ErrorCode.InternalError,
172
+ message: statusMessage,
173
+ },
174
+ };
175
+ }
176
+ function buildTaskCompletionUpdate(result, tool) {
177
+ const isError = isObject(result) && 'isError' in result && result.isError === true;
178
+ return {
179
+ status: isError ? 'failed' : 'completed',
180
+ statusMessage: isError
181
+ ? (tryReadToolErrorMessage(result) ?? 'Execution failed')
182
+ : (tool.getCompletionStatusMessage?.(result) ??
183
+ 'Task completed successfully.'),
184
+ result,
185
+ };
186
+ }
187
+ async function runTaskToolExecution(params) {
188
+ const { server, taskId, args, tool, meta, sessionId, sendNotification } = params;
189
+ return runWithRequestContext({
190
+ requestId: taskId,
191
+ operationId: taskId,
192
+ ...(sessionId ? { sessionId } : {}),
193
+ }, async () => {
194
+ const controller = attachAbortController(taskId);
195
+ const progressState = { closed: false };
196
+ try {
197
+ logInfo('Task execution started', { taskId, tool: tool.name }, Loggers.LOG_TASKS);
198
+ const relatedMeta = buildRelatedTaskMeta(taskId, meta);
199
+ const result = await tool.execute(args, {
200
+ signal: controller.signal,
201
+ requestId: taskId,
202
+ _meta: relatedMeta,
203
+ progressState,
204
+ canReportProgress: () => taskManager.getTask(taskId)?.status === 'working',
205
+ ...compact({ sendNotification }),
206
+ onProgress: (progress, message, total) => {
207
+ const current = taskManager.getTask(taskId);
208
+ if (current?.status === 'working' &&
209
+ (current.statusMessage !== message ||
210
+ current.progress !== progress ||
211
+ (total !== undefined && current.total !== total))) {
212
+ updateTaskAndEmitStatus(server, taskId, {
213
+ statusMessage: message,
214
+ progress,
215
+ ...(total !== undefined ? { total } : {}),
216
+ });
217
+ }
218
+ },
219
+ });
220
+ const completionUpdate = buildTaskCompletionUpdate(result, tool);
221
+ updateTaskAndEmitStatus(server, taskId, completionUpdate);
222
+ if (completionUpdate.status === 'completed') {
223
+ logInfo('Task execution completed', { taskId, tool: tool.name }, Loggers.LOG_TASKS);
224
+ }
225
+ else {
226
+ logWarn('Task execution completed with tool error result', { taskId, tool: tool.name }, Loggers.LOG_TASKS);
227
+ }
228
+ }
229
+ catch (error) {
230
+ logError('Task execution failed', {
231
+ taskId,
232
+ tool: tool.name,
233
+ error: getErrorMessage(error),
234
+ }, Loggers.LOG_TASKS);
235
+ updateTaskAndEmitStatus(server, taskId, buildTaskFailureState(error));
236
+ }
237
+ finally {
238
+ progressState.closed = true;
239
+ taskAbortControllers.delete(taskId);
240
+ }
241
+ });
242
+ }
243
+ function extractRawUrl(args) {
244
+ const url = args?.['url'];
245
+ return typeof url === 'string' ? url : 'unknown';
246
+ }
247
+ function tryParseArguments(tool, args) {
248
+ try {
249
+ return { ok: true, value: tool.parseArguments(args) };
250
+ }
251
+ catch (error) {
252
+ if (error instanceof McpError) {
253
+ return {
254
+ ok: false,
255
+ response: handleToolError(error, extractRawUrl(args)),
256
+ };
257
+ }
258
+ throw error;
259
+ }
260
+ }
261
+ export async function handleToolCallRequest(server, request, context) {
262
+ const { params } = request;
263
+ // Validate the tool name first so an unknown tool always produces MethodNotFound
264
+ const tool = getTaskCapableTool(server, params.name);
265
+ if (!tool) {
266
+ throw createMcpError(ErrorCode.MethodNotFound, `Unknown tool: ${params.name}`);
267
+ }
268
+ if (params.task) {
269
+ if (getTaskCapableToolSupport(server, params.name) === 'forbidden') {
270
+ throw createMcpError(ErrorCode.MethodNotFound, `Task mode is not supported for tool: ${params.name}`);
271
+ }
272
+ const parsed = tryParseArguments(tool, params.arguments);
273
+ if (!parsed.ok)
274
+ return parsed.response;
275
+ const task = taskManager.createTask(params.task.ttl !== undefined ? { ttl: params.task.ttl } : undefined, 'Task started', context.ownerKey);
276
+ logInfo('Task execution queued', {
277
+ taskId: task.taskId,
278
+ tool: params.name,
279
+ ...(params.task.ttl !== undefined ? { ttl: params.task.ttl } : {}),
280
+ }, Loggers.LOG_TASKS);
281
+ void runTaskToolExecution({
282
+ server,
283
+ taskId: task.taskId,
284
+ args: parsed.value,
285
+ tool,
286
+ ...compact({
287
+ meta: params._meta,
288
+ sessionId: context.sessionId,
289
+ sendNotification: context.sendNotification,
290
+ }),
291
+ });
292
+ return {
293
+ task: toTaskSummary(task),
294
+ ...(tool.immediateResponse
295
+ ? {
296
+ _meta: {
297
+ 'io.modelcontextprotocol/model-immediate-response': tool.immediateResponse,
298
+ },
299
+ }
300
+ : {}),
301
+ };
302
+ }
303
+ if (getTaskCapableToolSupport(server, params.name) === 'required') {
304
+ throw createMcpError(ErrorCode.MethodNotFound, `Task mode is required for tool: ${params.name}`);
305
+ }
306
+ const parsed = tryParseArguments(tool, params.arguments);
307
+ if (!parsed.ok)
308
+ return parsed.response;
309
+ const progressState = { closed: false };
310
+ logDebug('Executing task-capable tool inline', {
311
+ tool: params.name,
312
+ hasProgressToken: params._meta?.progressToken !== undefined,
313
+ }, Loggers.LOG_TASKS);
314
+ try {
315
+ return await tool.execute(parsed.value, {
316
+ ...buildToolHandlerExtra(context, params._meta),
317
+ progressState,
318
+ });
319
+ }
320
+ finally {
321
+ progressState.closed = true;
322
+ }
323
+ }
324
+ /* -------------------------------------------------------------------------------------------------
325
+ * Task handler schemas and registration
326
+ * ------------------------------------------------------------------------------------------------- */
327
+ const TaskGetSchema = z.looseObject({
328
+ method: z.literal('tasks/get', 'Expected "tasks/get"'),
329
+ params: z.looseObject({ taskId: z.string('Expected string') }, 'Expected object'),
330
+ }, 'Invalid request');
331
+ const TaskListSchema = z.looseObject({
332
+ method: z.literal('tasks/list', 'Expected "tasks/list"'),
333
+ params: z
334
+ .looseObject({
335
+ cursor: z.string('Expected string').optional(),
336
+ }, 'Expected object')
337
+ .optional(),
338
+ }, 'Invalid request');
339
+ const TaskCancelSchema = z.looseObject({
340
+ method: z.literal('tasks/cancel', 'Expected "tasks/cancel"'),
341
+ params: z.looseObject({ taskId: z.string('Expected string') }, 'Expected object'),
342
+ }, 'Invalid request');
343
+ const TaskResultSchema = z.looseObject({
344
+ method: z.literal('tasks/result', 'Expected "tasks/result"'),
345
+ params: z.looseObject({ taskId: z.string('Expected string') }, 'Expected object'),
346
+ }, 'Invalid request');
347
+ function resolveOwnerScopedExtra(extra) {
348
+ const parsedExtra = parseHandlerExtra(extra);
349
+ return {
350
+ parsedExtra,
351
+ ownerKey: resolveTaskOwnerKey(parsedExtra),
352
+ };
353
+ }
354
+ function throwStoredTaskError(task) {
355
+ if (task.error) {
356
+ throw createMcpError(task.error.code, task.error.message, task.error.data);
357
+ }
358
+ throw createMcpError(ErrorCode.InternalError, task.statusMessage ?? 'Execution failed', { taskId: task.taskId });
359
+ }
360
+ export function registerTaskHandlers(server, options) {
361
+ const sdkCallToolHandler = getSdkCallToolHandler(server);
362
+ const taskCapableToolsRegistered = hasRegisteredTaskCapableTools(server);
363
+ const requireInterception = options?.requireInterception ?? true;
364
+ if (!sdkCallToolHandler) {
365
+ if (taskCapableToolsRegistered && requireInterception) {
366
+ throw Error('Task-capable tools are registered but SDK tools/call interception is unavailable. Upgrade compatibility or disable strict interception with TASKS_REQUIRE_INTERCEPTION=false.');
367
+ }
368
+ logWarn('Task call interception disabled: SDK tools/call handler unavailable; task-capable tools require MCP SDK compatibility update', { sdkVersion: 'unknown' }, Loggers.LOG_TASKS);
369
+ }
370
+ if (sdkCallToolHandler) {
371
+ server.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
372
+ const parsedExtra = parseHandlerExtra(extra);
373
+ const requestId = parsedExtra?.requestId !== undefined
374
+ ? String(parsedExtra.requestId)
375
+ : randomUUID();
376
+ return runWithRequestContext({
377
+ requestId,
378
+ operationId: requestId,
379
+ ...(parsedExtra?.sessionId
380
+ ? { sessionId: parsedExtra.sessionId }
381
+ : {}),
382
+ }, () => {
383
+ const toolName = request.params.name;
384
+ // Only intercept task-capable tools managed by the local task registry.
385
+ // Delegate all other tools to the SDK handler to avoid shadowing future tools.
386
+ if (!hasTaskCapableTool(server, toolName)) {
387
+ return sdkCallToolHandler(request, extra);
388
+ }
389
+ const parsed = parseExtendedCallToolRequest(request);
390
+ const context = resolveToolCallContext(parsedExtra, parsed.params._meta);
391
+ logDebug('Intercepted task-capable tool call', {
392
+ tool: toolName,
393
+ taskRequested: parsed.params.task !== undefined,
394
+ hasProgressToken: parsed.params._meta?.progressToken !== undefined,
395
+ }, Loggers.LOG_TASKS);
396
+ return handleToolCallRequest(server, parsed, context);
397
+ });
398
+ });
399
+ }
400
+ server.server.setRequestHandler(TaskGetSchema, (request, extra) => {
401
+ const { taskId } = request.params;
402
+ const { ownerKey } = resolveOwnerScopedExtra(extra);
403
+ logDebug('tasks/get requested', { taskId }, Loggers.LOG_TASKS);
404
+ const task = taskManager.getTask(taskId, ownerKey);
405
+ if (!task)
406
+ throwTaskNotFound();
407
+ return toTaskSummary(task);
408
+ });
409
+ server.server.setRequestHandler(TaskResultSchema, async (request, extra) => {
410
+ const { taskId } = request.params;
411
+ const { parsedExtra, ownerKey } = resolveOwnerScopedExtra(extra);
412
+ logDebug('tasks/result requested', { taskId }, Loggers.LOG_TASKS);
413
+ const task = await taskManager.waitForTerminalTask(taskId, ownerKey, parsedExtra?.signal);
414
+ if (!task)
415
+ throwTaskNotFound();
416
+ try {
417
+ if (task.status === 'cancelled') {
418
+ throwStoredTaskError(task);
419
+ }
420
+ if (task.status === 'failed') {
421
+ if (task.error) {
422
+ throwStoredTaskError(task);
423
+ }
424
+ const failedResult = (task.result ?? null);
425
+ if (failedResult) {
426
+ return withRelatedTaskMeta(failedResult, task.taskId);
427
+ }
428
+ throwStoredTaskError(task);
429
+ }
430
+ const result = isServerResult(task.result)
431
+ ? task.result
432
+ : { content: [] };
433
+ return withRelatedTaskMeta(result, task.taskId);
434
+ }
435
+ finally {
436
+ // Shrink TTL only after the result has been fully constructed and
437
+ // is about to be delivered — avoids premature expiry if result
438
+ // construction throws.
439
+ taskManager.shrinkTtlAfterDelivery(taskId);
440
+ }
441
+ });
442
+ server.server.setRequestHandler(TaskListSchema, (request, extra) => {
443
+ const { ownerKey } = resolveOwnerScopedExtra(extra);
444
+ const cursor = request.params?.cursor;
445
+ logDebug('tasks/list requested', { hasCursor: cursor !== undefined }, Loggers.LOG_TASKS);
446
+ const { tasks, nextCursor } = taskManager.listTasks(cursor === undefined ? { ownerKey } : { ownerKey, cursor });
447
+ return {
448
+ tasks: tasks.map((task) => toTaskSummary(task)),
449
+ nextCursor,
450
+ };
451
+ });
452
+ server.server.setRequestHandler(TaskCancelSchema, (request, extra) => {
453
+ const { taskId } = request.params;
454
+ const { ownerKey } = resolveOwnerScopedExtra(extra);
455
+ logDebug('tasks/cancel requested', { taskId }, Loggers.LOG_TASKS);
456
+ const task = taskManager.cancelTask(taskId, ownerKey);
457
+ if (!task)
458
+ throwTaskNotFound();
459
+ abortTaskExecution(taskId);
460
+ emitTaskStatusNotification(server, task);
461
+ return toTaskSummary(task);
462
+ });
463
+ return {
464
+ interceptedToolsCall: sdkCallToolHandler !== null,
465
+ taskCapableToolsRegistered,
466
+ };
467
+ }
10
468
  const DEFAULT_TTL_MS = 60_000;
11
469
  const MIN_TTL_MS = 1_000;
12
470
  const MAX_TTL_MS = 86_400_000;
@@ -283,7 +741,7 @@ class TaskManager {
283
741
  return decoded.anchorTaskId;
284
742
  }
285
743
  async waitForTerminalTask(taskId, ownerKey, signal) {
286
- return waitForTerminalTaskWithDeadline({
744
+ return waitForTerminalTask({
287
745
  taskId,
288
746
  ownerKey,
289
747
  ...(signal && { signal }),
@@ -306,7 +764,6 @@ class TaskManager {
306
764
  }
307
765
  }
308
766
  }
309
- export const taskManager = new TaskManager();
310
767
  const MAX_CURSOR_LENGTH = 256;
311
768
  const MAX_ANCHOR_ID_LENGTH = 128;
312
769
  const CURSOR_SECRET = randomBytes(32);
@@ -344,3 +801,292 @@ export function decodeTaskCursor(cursor) {
344
801
  return null;
345
802
  }
346
803
  }
804
+ export function compact(obj) {
805
+ const result = {};
806
+ for (const key of Object.keys(obj)) {
807
+ if (obj[key] !== undefined) {
808
+ result[key] = obj[key];
809
+ }
810
+ }
811
+ return result;
812
+ }
813
+ function normalizeSendNotification(sendNotification) {
814
+ if (typeof sendNotification !== 'function')
815
+ return undefined;
816
+ const notify = sendNotification;
817
+ return async (notification) => {
818
+ await Promise.resolve(notify(notification));
819
+ };
820
+ }
821
+ function normalizeAuthInfo(authInfo) {
822
+ if (!isObject(authInfo))
823
+ return undefined;
824
+ const { clientId, token } = authInfo;
825
+ const normalized = {};
826
+ if (typeof clientId === 'string')
827
+ normalized.clientId = clientId;
828
+ if (typeof token === 'string')
829
+ normalized.token = token;
830
+ return normalized.clientId || normalized.token ? normalized : undefined;
831
+ }
832
+ export function parseHandlerExtra(extra) {
833
+ if (!isObject(extra))
834
+ return undefined;
835
+ const parsed = {};
836
+ const { authInfo, signal, requestId, sendNotification } = extra;
837
+ const sessionId = resolveSessionIdFromExtra(extra);
838
+ if (sessionId)
839
+ parsed.sessionId = sessionId;
840
+ const normalizedAuthInfo = normalizeAuthInfo(authInfo);
841
+ if (normalizedAuthInfo) {
842
+ parsed.authInfo = normalizedAuthInfo;
843
+ }
844
+ if (signal instanceof AbortSignal)
845
+ parsed.signal = signal;
846
+ if (typeof requestId === 'string' || typeof requestId === 'number') {
847
+ parsed.requestId = requestId;
848
+ }
849
+ const normalizedSendNotification = normalizeSendNotification(sendNotification);
850
+ if (normalizedSendNotification) {
851
+ parsed.sendNotification = normalizedSendNotification;
852
+ }
853
+ return parsed;
854
+ }
855
+ export function buildAuthenticatedOwnerKey(authInfo) {
856
+ const authClientId = typeof authInfo?.clientId === 'string' ? authInfo.clientId : '';
857
+ const authToken = typeof authInfo?.token === 'string' ? authInfo.token : '';
858
+ if (authClientId || authToken) {
859
+ return `auth:${hash('sha256', `${authClientId}:${authToken}`, 'hex')}`;
860
+ }
861
+ return undefined;
862
+ }
863
+ export function resolveTaskOwnerKey(extra) {
864
+ const authenticatedOwnerKey = buildAuthenticatedOwnerKey(extra?.authInfo);
865
+ if (authenticatedOwnerKey)
866
+ return authenticatedOwnerKey;
867
+ if (extra?.sessionId) {
868
+ return (resolveMcpSessionOwnerKey(extra.sessionId) ?? `session:${extra.sessionId}`);
869
+ }
870
+ return 'default';
871
+ }
872
+ function resolveRequestIdFromExtra(extra) {
873
+ if (!isObject(extra))
874
+ return undefined;
875
+ const { requestId } = extra;
876
+ if (typeof requestId === 'string')
877
+ return requestId;
878
+ if (typeof requestId === 'number')
879
+ return String(requestId);
880
+ return undefined;
881
+ }
882
+ function getHeaderString(headers, name) {
883
+ const value = headers[name];
884
+ if (typeof value === 'string')
885
+ return value;
886
+ if (!Array.isArray(value))
887
+ return undefined;
888
+ return value.find((entry) => typeof entry === 'string');
889
+ }
890
+ function resolveSessionIdFromExtra(extra) {
891
+ if (!isObject(extra))
892
+ return undefined;
893
+ const { sessionId } = extra;
894
+ if (typeof sessionId === 'string')
895
+ return sessionId;
896
+ const { requestInfo } = extra;
897
+ if (!isObject(requestInfo))
898
+ return undefined;
899
+ const { headers } = requestInfo;
900
+ if (!isObject(headers))
901
+ return undefined;
902
+ return (getHeaderString(headers, 'mcp-session-id') ??
903
+ getHeaderString(headers, 'x-mcp-session-id'));
904
+ }
905
+ function resolveToolExecutionContext(extra, requestMeta) {
906
+ return compact({
907
+ ownerKey: resolveTaskOwnerKey(extra),
908
+ sessionId: extra?.sessionId,
909
+ signal: extra?.signal,
910
+ requestId: extra?.requestId,
911
+ sendNotification: extra?.sendNotification,
912
+ requestMeta: sanitizeToolCallMeta(requestMeta),
913
+ });
914
+ }
915
+ export function resolveToolCallContext(extra, requestMeta) {
916
+ return resolveToolExecutionContext(extra, requestMeta);
917
+ }
918
+ export function buildToolHandlerExtra(context, requestMeta) {
919
+ return compact({
920
+ signal: context.signal,
921
+ requestId: context.requestId,
922
+ sendNotification: context.sendNotification,
923
+ _meta: sanitizeToolCallMeta(requestMeta ?? context.requestMeta),
924
+ });
925
+ }
926
+ export function withRequestContextIfMissing(handler) {
927
+ return async (params, extra) => {
928
+ const existingRequestId = getRequestId();
929
+ if (existingRequestId) {
930
+ return handler(params, extra);
931
+ }
932
+ const derivedRequestId = resolveRequestIdFromExtra(extra) ?? randomUUID();
933
+ const derivedSessionId = resolveSessionIdFromExtra(extra);
934
+ return runWithRequestContext({
935
+ requestId: derivedRequestId,
936
+ operationId: derivedRequestId,
937
+ ...(derivedSessionId ? { sessionId: derivedSessionId } : {}),
938
+ }, () => handler(params, extra));
939
+ };
940
+ }
941
+ export function isServerResult(value) {
942
+ return isObject(value) && Array.isArray(value['content']);
943
+ }
944
+ const taskCapableToolsByServer = new WeakMap();
945
+ function getServerToolMap(server) {
946
+ let toolMap = taskCapableToolsByServer.get(server);
947
+ if (toolMap)
948
+ return toolMap;
949
+ toolMap = new Map();
950
+ taskCapableToolsByServer.set(server, toolMap);
951
+ registerServerLifecycleCleanup(server, () => {
952
+ taskCapableToolsByServer.delete(server);
953
+ });
954
+ return toolMap;
955
+ }
956
+ export function registerTaskCapableTool(server, descriptor) {
957
+ getServerToolMap(server).set(descriptor.name, {
958
+ ...descriptor,
959
+ taskSupport: descriptor.taskSupport ?? 'optional',
960
+ });
961
+ }
962
+ export function unregisterTaskCapableTool(server, name) {
963
+ getServerToolMap(server).delete(name);
964
+ }
965
+ export function getTaskCapableTool(server, name) {
966
+ return getServerToolMap(server).get(name);
967
+ }
968
+ export function getTaskCapableToolSupport(server, name) {
969
+ return getServerToolMap(server).get(name)?.taskSupport;
970
+ }
971
+ export function hasTaskCapableTool(server, name) {
972
+ return getServerToolMap(server).has(name);
973
+ }
974
+ export function hasRegisteredTaskCapableTools(server) {
975
+ return getServerToolMap(server).size > 0;
976
+ }
977
+ export function setTaskCapableToolSupport(server, name, support) {
978
+ const descriptor = getServerToolMap(server).get(name);
979
+ if (!descriptor)
980
+ return;
981
+ descriptor.taskSupport = support;
982
+ }
983
+ export class TaskWaiterRegistry {
984
+ isTerminalStatus;
985
+ waiters = new Map();
986
+ constructor(isTerminalStatus) {
987
+ this.isTerminalStatus = isTerminalStatus;
988
+ }
989
+ add(taskId, waiter) {
990
+ let set = this.waiters.get(taskId);
991
+ if (!set) {
992
+ set = new Set();
993
+ this.waiters.set(taskId, set);
994
+ }
995
+ set.add(waiter);
996
+ }
997
+ remove(taskId, waiter) {
998
+ if (!waiter)
999
+ return;
1000
+ const set = this.waiters.get(taskId);
1001
+ if (!set)
1002
+ return;
1003
+ set.delete(waiter);
1004
+ if (set.size === 0) {
1005
+ this.waiters.delete(taskId);
1006
+ }
1007
+ }
1008
+ notify(task) {
1009
+ if (!this.isTerminalStatus(task.status))
1010
+ return;
1011
+ const waiters = this.waiters.get(task.taskId);
1012
+ if (!waiters)
1013
+ return;
1014
+ this.waiters.delete(task.taskId);
1015
+ for (const waiter of waiters)
1016
+ waiter(task);
1017
+ }
1018
+ }
1019
+ export async function waitForTerminalTask(options) {
1020
+ const task = options.lookupTask(options.taskId, options.ownerKey);
1021
+ if (!task)
1022
+ return undefined;
1023
+ if (options.isTerminalStatus(task.status))
1024
+ return task;
1025
+ const deadlineMs = task._createdAtMs + task.ttl;
1026
+ const { promise, resolve, reject } = Promise.withResolvers();
1027
+ const resolveInContext = AsyncLocalStorage.bind((value) => {
1028
+ resolve(value);
1029
+ });
1030
+ const rejectInContext = AsyncLocalStorage.bind((error) => {
1031
+ reject(toError(error));
1032
+ });
1033
+ let settled = false;
1034
+ let waiter = null;
1035
+ let deadlineTimeout;
1036
+ const cleanup = () => {
1037
+ if (deadlineTimeout) {
1038
+ deadlineTimeout.cancel();
1039
+ deadlineTimeout = undefined;
1040
+ }
1041
+ if (options.signal) {
1042
+ options.signal.removeEventListener('abort', onAbort);
1043
+ }
1044
+ };
1045
+ const settleOnce = (fn) => {
1046
+ if (settled)
1047
+ return;
1048
+ settled = true;
1049
+ fn();
1050
+ };
1051
+ const onAbort = () => {
1052
+ settleOnce(() => {
1053
+ cleanup();
1054
+ options.registry.remove(options.taskId, waiter);
1055
+ rejectInContext(createMcpError(ErrorCode.ConnectionClosed, 'Request was cancelled'));
1056
+ });
1057
+ };
1058
+ waiter = (updated) => {
1059
+ settleOnce(() => {
1060
+ cleanup();
1061
+ if (updated.ownerKey !== options.ownerKey) {
1062
+ resolveInContext(undefined);
1063
+ return;
1064
+ }
1065
+ resolveInContext(updated);
1066
+ });
1067
+ };
1068
+ if (options.signal?.aborted) {
1069
+ onAbort();
1070
+ return promise;
1071
+ }
1072
+ options.registry.add(options.taskId, waiter);
1073
+ if (options.signal) {
1074
+ options.signal.addEventListener('abort', onAbort, { once: true });
1075
+ }
1076
+ const timeoutMs = Math.max(0, deadlineMs - Date.now());
1077
+ deadlineTimeout = createUnrefTimeout(timeoutMs, { timeout: true });
1078
+ void deadlineTimeout.promise
1079
+ .then(() => {
1080
+ settleOnce(() => {
1081
+ cleanup();
1082
+ options.registry.remove(options.taskId, waiter);
1083
+ options.removeTask(options.taskId);
1084
+ rejectInContext(createMcpError(ErrorCode.InvalidParams, 'Task expired', {
1085
+ taskId: options.taskId,
1086
+ }));
1087
+ });
1088
+ })
1089
+ .catch(rejectInContext);
1090
+ return promise;
1091
+ }
1092
+ export const taskManager = new TaskManager();