@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.
- package/dist/http/auth.d.ts +2 -2
- package/dist/http/auth.d.ts.map +1 -1
- package/dist/http/auth.js +4 -5
- package/dist/http/index.d.ts +6 -0
- package/dist/http/index.d.ts.map +1 -0
- package/dist/http/index.js +5 -0
- package/dist/http/native.d.ts +73 -0
- package/dist/http/native.d.ts.map +1 -1
- package/dist/http/native.js +554 -10
- package/dist/http/rate-limit.d.ts +1 -1
- package/dist/http/rate-limit.d.ts.map +1 -1
- package/dist/http/rate-limit.js +3 -4
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +67 -6
- package/dist/lib/config.js +2 -2
- package/dist/lib/core.d.ts +56 -4
- package/dist/lib/core.d.ts.map +1 -1
- package/dist/lib/core.js +155 -4
- package/dist/lib/error/classes.d.ts +19 -0
- package/dist/lib/error/classes.d.ts.map +1 -0
- package/dist/lib/error/classes.js +107 -0
- package/dist/lib/error/classify.d.ts +4 -0
- package/dist/lib/error/classify.d.ts.map +1 -0
- package/dist/lib/error/classify.js +154 -0
- package/dist/lib/error/codes.d.ts +23 -0
- package/dist/lib/error/codes.d.ts.map +1 -0
- package/dist/lib/error/codes.js +22 -0
- package/dist/lib/error/index.d.ts +6 -0
- package/dist/lib/error/index.d.ts.map +1 -0
- package/dist/lib/error/index.js +5 -0
- package/dist/lib/{error-messages.d.ts → error/messages.d.ts} +2 -2
- package/dist/lib/error/messages.d.ts.map +1 -0
- package/dist/lib/{error-messages.js → error/messages.js} +2 -2
- package/dist/lib/{tool-errors.d.ts → error/payload.d.ts} +7 -13
- package/dist/lib/error/payload.d.ts.map +1 -0
- package/dist/lib/error/payload.js +108 -0
- package/dist/lib/mcp-interop.d.ts.map +1 -1
- package/dist/lib/mcp-interop.js +4 -6
- package/dist/lib/net/http.d.ts.map +1 -0
- package/dist/lib/{http.js → net/http.js} +4 -7
- package/dist/lib/net/index.d.ts +4 -0
- package/dist/lib/net/index.d.ts.map +1 -0
- package/dist/lib/net/index.js +3 -0
- package/dist/lib/{fetch-pipeline.d.ts → net/pipeline.d.ts} +3 -3
- package/dist/lib/net/pipeline.d.ts.map +1 -0
- package/dist/lib/{fetch-pipeline.js → net/pipeline.js} +3 -5
- package/dist/lib/{url.d.ts → net/url.d.ts} +1 -1
- package/dist/lib/net/url.d.ts.map +1 -0
- package/dist/lib/{url.js → net/url.js} +3 -5
- package/dist/lib/utils.d.ts +2 -18
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +29 -104
- package/dist/resources/index.d.ts.map +1 -1
- package/dist/resources/index.js +8 -5
- package/dist/schemas.d.ts +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +7 -9
- package/dist/tasks/index.d.ts +2 -0
- package/dist/tasks/index.d.ts.map +1 -0
- package/dist/tasks/index.js +1 -0
- package/dist/tasks/manager.d.ts +123 -1
- package/dist/tasks/manager.d.ts.map +1 -1
- package/dist/tasks/manager.js +745 -10
- package/dist/tools/{fetch-url.d.ts → index.d.ts} +4 -5
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/{fetch-url.js → index.js} +6 -8
- package/dist/transform/index.d.ts +279 -0
- package/dist/transform/index.d.ts.map +1 -0
- package/dist/transform/index.js +5234 -0
- package/package.json +2 -2
- package/dist/cli.d.ts +0 -19
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -65
- package/dist/http/health.d.ts +0 -8
- package/dist/http/health.d.ts.map +0 -1
- package/dist/http/health.js +0 -152
- package/dist/http/helpers.d.ts +0 -68
- package/dist/http/helpers.d.ts.map +0 -1
- package/dist/http/helpers.js +0 -402
- package/dist/lib/error-codes.d.ts +0 -13
- package/dist/lib/error-codes.d.ts.map +0 -1
- package/dist/lib/error-codes.js +0 -12
- package/dist/lib/error-messages.d.ts.map +0 -1
- package/dist/lib/fetch-pipeline.d.ts.map +0 -1
- package/dist/lib/http.d.ts.map +0 -1
- package/dist/lib/logger-names.d.ts +0 -16
- package/dist/lib/logger-names.d.ts.map +0 -1
- package/dist/lib/logger-names.js +0 -15
- package/dist/lib/session.d.ts +0 -44
- package/dist/lib/session.d.ts.map +0 -1
- package/dist/lib/session.js +0 -137
- package/dist/lib/tool-errors.d.ts.map +0 -1
- package/dist/lib/tool-errors.js +0 -253
- package/dist/lib/url.d.ts.map +0 -1
- package/dist/lib/zod.d.ts +0 -3
- package/dist/lib/zod.d.ts.map +0 -1
- package/dist/lib/zod.js +0 -27
- package/dist/tasks/call-contract.d.ts +0 -25
- package/dist/tasks/call-contract.d.ts.map +0 -1
- package/dist/tasks/call-contract.js +0 -59
- package/dist/tasks/execution.d.ts +0 -16
- package/dist/tasks/execution.d.ts.map +0 -1
- package/dist/tasks/execution.js +0 -241
- package/dist/tasks/handlers.d.ts +0 -11
- package/dist/tasks/handlers.d.ts.map +0 -1
- package/dist/tasks/handlers.js +0 -157
- package/dist/tasks/owner.d.ts +0 -43
- package/dist/tasks/owner.d.ts.map +0 -1
- package/dist/tasks/owner.js +0 -144
- package/dist/tasks/registry.d.ts +0 -20
- package/dist/tasks/registry.d.ts.map +0 -1
- package/dist/tasks/registry.js +0 -40
- package/dist/tasks/waiters.d.ts +0 -27
- package/dist/tasks/waiters.d.ts.map +0 -1
- package/dist/tasks/waiters.js +0 -114
- package/dist/tools/fetch-url.d.ts.map +0 -1
- package/dist/transform/dom-prep.d.ts +0 -16
- package/dist/transform/dom-prep.d.ts.map +0 -1
- package/dist/transform/dom-prep.js +0 -1287
- package/dist/transform/html-translators.d.ts +0 -5
- package/dist/transform/html-translators.d.ts.map +0 -1
- package/dist/transform/html-translators.js +0 -697
- package/dist/transform/markdown-cleanup.d.ts +0 -10
- package/dist/transform/markdown-cleanup.d.ts.map +0 -1
- package/dist/transform/markdown-cleanup.js +0 -542
- package/dist/transform/metadata.d.ts +0 -18
- package/dist/transform/metadata.d.ts.map +0 -1
- package/dist/transform/metadata.js +0 -462
- package/dist/transform/next-flight.d.ts +0 -2
- package/dist/transform/next-flight.d.ts.map +0 -1
- package/dist/transform/next-flight.js +0 -374
- package/dist/transform/shared.d.ts +0 -8
- package/dist/transform/shared.d.ts.map +0 -1
- package/dist/transform/shared.js +0 -137
- package/dist/transform/transform.d.ts +0 -38
- package/dist/transform/transform.d.ts.map +0 -1
- package/dist/transform/transform.js +0 -1042
- package/dist/transform/types.d.ts +0 -124
- package/dist/transform/types.d.ts.map +0 -1
- package/dist/transform/types.js +0 -5
- package/dist/transform/worker-pool.d.ts +0 -76
- package/dist/transform/worker-pool.d.ts.map +0 -1
- package/dist/transform/worker-pool.js +0 -725
- /package/dist/lib/{http.d.ts → net/http.d.ts} +0 -0
package/dist/tasks/manager.js
CHANGED
|
@@ -1,12 +1,459 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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 {
|
|
5
|
-
import { config, logInfo, logWarn } from '../lib/core.js';
|
|
6
|
-
import {
|
|
7
|
-
import { createMcpError } from '../lib/mcp-interop.js';
|
|
8
|
-
import { isObject, timingSafeEqualUtf8 } from '../lib/utils.js';
|
|
9
|
-
|
|
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
|
|
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();
|