@j0hanz/fetch-url-mcp 1.12.7 → 1.12.8

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