@j0hanz/fetch-url-mcp 1.12.13 → 2.0.1
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/LICENSE +21 -21
- package/README.md +815 -809
- package/dist/assets/logo.svg +53 -53
- package/dist/http/auth.d.ts +7 -3
- package/dist/http/auth.d.ts.map +1 -1
- package/dist/http/auth.js +65 -37
- package/dist/http/helpers.d.ts +21 -0
- package/dist/http/helpers.d.ts.map +1 -0
- package/dist/http/helpers.js +31 -0
- package/dist/http/index.d.ts +1 -0
- package/dist/http/index.d.ts.map +1 -1
- package/dist/http/index.js +1 -0
- package/dist/http/native.d.ts +6 -24
- package/dist/http/native.d.ts.map +1 -1
- package/dist/http/native.js +141 -86
- 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 -9
- package/dist/index.js +0 -0
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +1 -0
- package/dist/lib/core.d.ts +4 -3
- package/dist/lib/core.d.ts.map +1 -1
- package/dist/lib/core.js +4 -1
- package/dist/lib/error/classify.d.ts.map +1 -1
- package/dist/lib/error/classify.js +27 -6
- package/dist/lib/error/payload.d.ts +2 -2
- package/dist/lib/error/payload.d.ts.map +1 -1
- package/dist/lib/error/payload.js +2 -2
- package/dist/lib/mcp-interop.d.ts +9 -108
- package/dist/lib/mcp-interop.d.ts.map +1 -1
- package/dist/lib/mcp-interop.js +39 -240
- package/dist/lib/net/http.d.ts.map +1 -1
- package/dist/lib/net/http.js +4 -17
- package/dist/resources/index.d.ts +1 -1
- package/dist/resources/index.d.ts.map +1 -1
- package/dist/resources/index.js +67 -60
- package/dist/server.d.ts +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +39 -34
- package/dist/tasks/adapter.d.ts +15 -0
- package/dist/tasks/adapter.d.ts.map +1 -0
- package/dist/tasks/adapter.js +138 -0
- package/dist/tasks/manager.d.ts +14 -82
- package/dist/tasks/manager.d.ts.map +1 -1
- package/dist/tasks/manager.js +48 -607
- package/dist/tasks/store.d.ts +33 -19
- package/dist/tasks/store.d.ts.map +1 -1
- package/dist/tasks/store.js +143 -80
- package/dist/tools/index.d.ts +7 -13
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +153 -58
- package/dist/transform/index.js +1 -1
- package/package.json +110 -108
package/dist/tasks/manager.js
CHANGED
|
@@ -1,72 +1,13 @@
|
|
|
1
|
-
import {} from '@modelcontextprotocol/
|
|
2
|
-
import {
|
|
3
|
-
import { hash, randomUUID } from 'node:crypto';
|
|
1
|
+
import { ProtocolErrorCode, RELATED_TASK_META_KEY, } from '@modelcontextprotocol/server';
|
|
2
|
+
import { hash } from 'node:crypto';
|
|
4
3
|
import { z } from 'zod';
|
|
5
4
|
import { config } from '../lib/config.js';
|
|
6
|
-
import {
|
|
7
|
-
import { getErrorMessage
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
5
|
+
import { logDebug, logError, Loggers, logWarn, resolveMcpSessionOwnerKey, } from '../lib/core.js';
|
|
6
|
+
import { getErrorMessage } from '../lib/error/index.js';
|
|
7
|
+
import { createProtocolError } from '../lib/mcp-interop.js';
|
|
8
|
+
import { isObject } from '../lib/utils.js';
|
|
10
9
|
import { decodeTaskCursor, encodeTaskCursor, taskManager, TaskWaiterRegistry, waitForTerminalTask, } from './store.js';
|
|
11
|
-
/*
|
|
12
|
-
* Module map:
|
|
13
|
-
* - call-tool request parsing
|
|
14
|
-
* - Abort-controller management for in-flight task executions
|
|
15
|
-
* - Task notification and validation helpers
|
|
16
|
-
* - Execution pipeline
|
|
17
|
-
* - Task handler schemas and registration
|
|
18
|
-
* - Handler extra parsing & owner-key resolution
|
|
19
|
-
* Own task lifecycle and MCP task wiring here. Keep tool business logic and HTTP transport details elsewhere.
|
|
20
|
-
*/
|
|
21
|
-
const MIN_TASK_KEEP_ALIVE_MS = 1_000;
|
|
22
|
-
const MAX_TASK_KEEP_ALIVE_MS = 86_400_000;
|
|
23
|
-
const RELATED_TASK_META_KEY = 'modelcontextprotocol.io/related-task';
|
|
24
|
-
const taskMetaSchema = z.strictObject({
|
|
25
|
-
taskId: z.string().min(1, 'Task id required'),
|
|
26
|
-
keepAlive: z
|
|
27
|
-
.number()
|
|
28
|
-
.int()
|
|
29
|
-
.min(MIN_TASK_KEEP_ALIVE_MS, `Minimum ${MIN_TASK_KEEP_ALIVE_MS}ms`)
|
|
30
|
-
.max(MAX_TASK_KEEP_ALIVE_MS, `Maximum ${MAX_TASK_KEEP_ALIVE_MS}ms`)
|
|
31
|
-
.optional(),
|
|
32
|
-
});
|
|
33
|
-
const relatedTaskMetaSchema = z.strictObject({
|
|
34
|
-
taskId: z.string(),
|
|
35
|
-
});
|
|
36
|
-
const toolCallMetaSchema = z.looseObject({
|
|
37
|
-
progressToken: z.union([z.string(), z.number()]).optional(),
|
|
38
|
-
'modelcontextprotocol.io/task': taskMetaSchema.optional(),
|
|
39
|
-
[RELATED_TASK_META_KEY]: relatedTaskMetaSchema.optional(),
|
|
40
|
-
});
|
|
41
|
-
const sdkTaskParamsSchema = z.object({ ttl: z.number().optional() }).optional();
|
|
42
|
-
export const extendedCallToolRequestSchema = z.looseObject({
|
|
43
|
-
method: z.literal('tools/call'),
|
|
44
|
-
params: z.strictObject({
|
|
45
|
-
name: z.string().min(1, 'Tool name required'),
|
|
46
|
-
arguments: z.record(z.string(), z.unknown()).optional(),
|
|
47
|
-
_meta: toolCallMetaSchema.optional(),
|
|
48
|
-
task: sdkTaskParamsSchema,
|
|
49
|
-
}),
|
|
50
|
-
});
|
|
51
10
|
export { decodeTaskCursor, encodeTaskCursor, taskManager, TaskWaiterRegistry, waitForTerminalTask, };
|
|
52
|
-
function getTaskMeta(params) {
|
|
53
|
-
const legacyMeta = params._meta?.['modelcontextprotocol.io/task'];
|
|
54
|
-
if (legacyMeta)
|
|
55
|
-
return legacyMeta;
|
|
56
|
-
if (params.task) {
|
|
57
|
-
return {
|
|
58
|
-
taskId: randomUUID(),
|
|
59
|
-
...(params.task.ttl !== undefined ? { keepAlive: params.task.ttl } : {}),
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
return undefined;
|
|
63
|
-
}
|
|
64
|
-
export function parseExtendedCallToolRequest(request) {
|
|
65
|
-
const parsed = extendedCallToolRequestSchema.safeParse(request);
|
|
66
|
-
if (parsed.success)
|
|
67
|
-
return parsed.data;
|
|
68
|
-
throw createMcpError(ErrorCode.InvalidParams, formatZodError(parsed.error));
|
|
69
|
-
}
|
|
70
11
|
export function sanitizeToolCallMeta(meta) {
|
|
71
12
|
if (!meta)
|
|
72
13
|
return undefined;
|
|
@@ -103,14 +44,14 @@ function withRelatedTaskSummaryMeta(payload, taskId) {
|
|
|
103
44
|
// Intentionally process-global (not session-scoped): abortAllTaskExecutions() is called
|
|
104
45
|
// during SIGTERM/SIGINT shutdown to cancel every in-flight task across all sessions.
|
|
105
46
|
const taskAbortControllers = new Map();
|
|
106
|
-
function detachAbortController(taskId) {
|
|
47
|
+
export function detachAbortController(taskId) {
|
|
107
48
|
const controller = taskAbortControllers.get(taskId);
|
|
108
49
|
if (controller) {
|
|
109
50
|
taskAbortControllers.delete(taskId);
|
|
110
51
|
}
|
|
111
52
|
return controller;
|
|
112
53
|
}
|
|
113
|
-
function attachAbortController(taskId) {
|
|
54
|
+
export function attachAbortController(taskId) {
|
|
114
55
|
detachAbortController(taskId)?.abort();
|
|
115
56
|
if (taskAbortControllers.size >= config.tasks.maxTotal) {
|
|
116
57
|
logWarn('Abort controller map reached task capacity — possible leak', {
|
|
@@ -143,14 +84,10 @@ export function toTaskSummary(task) {
|
|
|
143
84
|
taskId: task.taskId,
|
|
144
85
|
status: task.status,
|
|
145
86
|
...(task.statusMessage ? { statusMessage: task.statusMessage } : {}),
|
|
146
|
-
...(task.progress !== undefined ? { progress: task.progress } : {}),
|
|
147
|
-
...(task.total !== undefined ? { total: task.total } : {}),
|
|
148
87
|
createdAt: task.createdAt,
|
|
149
88
|
lastUpdatedAt: task.lastUpdatedAt,
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
ttl: task.keepAlive,
|
|
153
|
-
pollInterval: task.pollFrequency,
|
|
89
|
+
ttl: task.ttl,
|
|
90
|
+
pollInterval: task.pollInterval,
|
|
154
91
|
};
|
|
155
92
|
}
|
|
156
93
|
export function emitTaskStatusNotification(server, task) {
|
|
@@ -169,444 +106,51 @@ export function emitTaskStatusNotification(server, task) {
|
|
|
169
106
|
}, Loggers.LOG_TASKS);
|
|
170
107
|
});
|
|
171
108
|
}
|
|
172
|
-
function emitTaskCreatedNotification(server, task) {
|
|
173
|
-
if (!config.tasks.emitStatusNotifications || !server.isConnected())
|
|
174
|
-
return;
|
|
175
|
-
void server.server
|
|
176
|
-
.notification({
|
|
177
|
-
method: 'notifications/tasks/created',
|
|
178
|
-
params: {
|
|
179
|
-
_meta: {
|
|
180
|
-
[RELATED_TASK_META_KEY]: { taskId: task.taskId },
|
|
181
|
-
},
|
|
182
|
-
},
|
|
183
|
-
})
|
|
184
|
-
.catch((error) => {
|
|
185
|
-
logError('Failed to send task created notification', {
|
|
186
|
-
taskId: task.taskId,
|
|
187
|
-
error: getErrorMessage(error),
|
|
188
|
-
}, Loggers.LOG_TASKS);
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
109
|
export function throwTaskNotFound() {
|
|
192
|
-
throw
|
|
193
|
-
}
|
|
194
|
-
/* -------------------------------------------------------------------------------------------------
|
|
195
|
-
* Execution pipeline
|
|
196
|
-
* ------------------------------------------------------------------------------------------------- */
|
|
197
|
-
function updateTaskAndEmitStatus(server, taskId, update) {
|
|
198
|
-
taskManager.updateTask(taskId, update);
|
|
199
|
-
const task = taskManager.getTask(taskId);
|
|
200
|
-
if (task)
|
|
201
|
-
emitTaskStatusNotification(server, task);
|
|
202
|
-
}
|
|
203
|
-
function buildTaskFailureState(error) {
|
|
204
|
-
const mcpErrorMessage = error instanceof McpError ? stripMcpErrorPrefix(error.message) : undefined;
|
|
205
|
-
const statusMessage = mcpErrorMessage ?? getErrorMessage(error);
|
|
206
|
-
if (error instanceof McpError) {
|
|
207
|
-
return {
|
|
208
|
-
status: 'failed',
|
|
209
|
-
statusMessage,
|
|
210
|
-
error: {
|
|
211
|
-
code: error.code,
|
|
212
|
-
...(error.data !== undefined ? { data: error.data } : {}),
|
|
213
|
-
message: statusMessage,
|
|
214
|
-
},
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
return {
|
|
218
|
-
status: 'failed',
|
|
219
|
-
statusMessage,
|
|
220
|
-
error: {
|
|
221
|
-
code: ErrorCode.InternalError,
|
|
222
|
-
message: statusMessage,
|
|
223
|
-
},
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
function buildTaskCompletionUpdate(result, tool) {
|
|
227
|
-
const isError = isObject(result) && 'isError' in result && result.isError === true;
|
|
228
|
-
const errorMessage = tryReadToolErrorMessage(result) ?? 'Execution failed';
|
|
229
|
-
return {
|
|
230
|
-
status: isError ? 'failed' : 'completed',
|
|
231
|
-
statusMessage: isError
|
|
232
|
-
? errorMessage
|
|
233
|
-
: (tool.getCompletionStatusMessage?.(result) ??
|
|
234
|
-
'Task completed successfully.'),
|
|
235
|
-
result,
|
|
236
|
-
...(isError
|
|
237
|
-
? {
|
|
238
|
-
error: {
|
|
239
|
-
code: ErrorCode.InternalError,
|
|
240
|
-
message: errorMessage,
|
|
241
|
-
},
|
|
242
|
-
}
|
|
243
|
-
: {}),
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
async function runTaskToolExecution(params) {
|
|
247
|
-
const { server, taskId, args, tool, meta, sessionId, sendNotification } = params;
|
|
248
|
-
return runWithRequestContext({
|
|
249
|
-
requestId: taskId,
|
|
250
|
-
operationId: taskId,
|
|
251
|
-
...(sessionId ? { sessionId } : {}),
|
|
252
|
-
}, () => runWithTraceContext(meta, async () => {
|
|
253
|
-
const controller = attachAbortController(taskId);
|
|
254
|
-
const progressState = { closed: false };
|
|
255
|
-
try {
|
|
256
|
-
updateTaskAndEmitStatus(server, taskId, {
|
|
257
|
-
status: 'working',
|
|
258
|
-
statusMessage: 'Task started',
|
|
259
|
-
});
|
|
260
|
-
logInfo('Task execution started', { taskId, tool: tool.name }, Loggers.LOG_TASKS);
|
|
261
|
-
const relatedMeta = buildRelatedTaskMeta(taskId, meta);
|
|
262
|
-
const result = await tool.execute(args, {
|
|
263
|
-
signal: controller.signal,
|
|
264
|
-
requestId: taskId,
|
|
265
|
-
_meta: relatedMeta,
|
|
266
|
-
progressState,
|
|
267
|
-
canReportProgress: () => taskManager.getTask(taskId)?.status === 'working',
|
|
268
|
-
...compact({ sendNotification }),
|
|
269
|
-
onProgress: (progress, message, total) => {
|
|
270
|
-
const current = taskManager.getTask(taskId);
|
|
271
|
-
if (current?.status === 'working' &&
|
|
272
|
-
(current.statusMessage !== message ||
|
|
273
|
-
current.progress !== progress ||
|
|
274
|
-
(total !== undefined && current.total !== total))) {
|
|
275
|
-
updateTaskAndEmitStatus(server, taskId, {
|
|
276
|
-
statusMessage: message,
|
|
277
|
-
progress,
|
|
278
|
-
...(total !== undefined ? { total } : {}),
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
},
|
|
282
|
-
});
|
|
283
|
-
const completionUpdate = buildTaskCompletionUpdate(result, tool);
|
|
284
|
-
updateTaskAndEmitStatus(server, taskId, completionUpdate);
|
|
285
|
-
if (completionUpdate.status === 'completed') {
|
|
286
|
-
logInfo('Task execution completed', { taskId, tool: tool.name }, Loggers.LOG_TASKS);
|
|
287
|
-
}
|
|
288
|
-
else {
|
|
289
|
-
logWarn('Task execution completed with tool error result', { taskId, tool: tool.name }, Loggers.LOG_TASKS);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
catch (error) {
|
|
293
|
-
logError('Task execution failed', {
|
|
294
|
-
taskId,
|
|
295
|
-
tool: tool.name,
|
|
296
|
-
error: getErrorMessage(error),
|
|
297
|
-
}, Loggers.LOG_TASKS);
|
|
298
|
-
updateTaskAndEmitStatus(server, taskId, buildTaskFailureState(error));
|
|
299
|
-
}
|
|
300
|
-
finally {
|
|
301
|
-
progressState.closed = true;
|
|
302
|
-
detachAbortController(taskId);
|
|
303
|
-
}
|
|
304
|
-
}));
|
|
305
|
-
}
|
|
306
|
-
function extractRawUrl(args) {
|
|
307
|
-
const url = args?.['url'];
|
|
308
|
-
return typeof url === 'string' ? url : 'unknown';
|
|
309
|
-
}
|
|
310
|
-
function tryParseArguments(tool, args) {
|
|
311
|
-
try {
|
|
312
|
-
return { ok: true, value: tool.parseArguments(args) };
|
|
313
|
-
}
|
|
314
|
-
catch (error) {
|
|
315
|
-
if (error instanceof McpError) {
|
|
316
|
-
return {
|
|
317
|
-
ok: false,
|
|
318
|
-
response: handleToolError(error, extractRawUrl(args)),
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
throw error;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
function validateTaskSupport(server, toolName, isTaskMode) {
|
|
325
|
-
const support = getTaskCapableToolSupport(server, toolName);
|
|
326
|
-
if (isTaskMode && support === 'forbidden') {
|
|
327
|
-
throw createMcpError(ErrorCode.MethodNotFound, `Task mode is not supported for tool: ${toolName}`);
|
|
328
|
-
}
|
|
329
|
-
if (!isTaskMode && support === 'required') {
|
|
330
|
-
throw createMcpError(ErrorCode.MethodNotFound, `Task mode is required for tool: ${toolName}`);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
function enqueueTaskToolExecution(server, tool, params, taskMeta, context, parsedArgs) {
|
|
334
|
-
const task = taskManager.createTask({
|
|
335
|
-
taskId: taskMeta.taskId,
|
|
336
|
-
...(taskMeta.keepAlive !== undefined
|
|
337
|
-
? { keepAlive: taskMeta.keepAlive }
|
|
338
|
-
: {}),
|
|
339
|
-
}, 'Task submitted', context.ownerKey);
|
|
340
|
-
emitTaskCreatedNotification(server, task);
|
|
341
|
-
logInfo('Task execution queued', {
|
|
342
|
-
taskId: task.taskId,
|
|
343
|
-
tool: params.name,
|
|
344
|
-
...(taskMeta.keepAlive !== undefined
|
|
345
|
-
? { keepAlive: taskMeta.keepAlive }
|
|
346
|
-
: {}),
|
|
347
|
-
}, Loggers.LOG_TASKS);
|
|
348
|
-
void runTaskToolExecution({
|
|
349
|
-
server,
|
|
350
|
-
taskId: task.taskId,
|
|
351
|
-
args: parsedArgs,
|
|
352
|
-
tool,
|
|
353
|
-
...compact({
|
|
354
|
-
meta: params._meta,
|
|
355
|
-
sessionId: context.sessionId,
|
|
356
|
-
sendNotification: context.sendNotification,
|
|
357
|
-
}),
|
|
358
|
-
});
|
|
359
|
-
return {
|
|
360
|
-
task: toTaskSummary(task),
|
|
361
|
-
...(tool.immediateResponse
|
|
362
|
-
? {
|
|
363
|
-
_meta: {
|
|
364
|
-
'io.modelcontextprotocol/model-immediate-response': tool.immediateResponse,
|
|
365
|
-
},
|
|
366
|
-
}
|
|
367
|
-
: {}),
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
export async function handleToolCallRequest(server, request, context) {
|
|
371
|
-
const { params } = request;
|
|
372
|
-
return runWithTraceContext(params._meta, async () => {
|
|
373
|
-
// Validate the tool name first so an unknown tool always produces MethodNotFound
|
|
374
|
-
const tool = getTaskCapableTool(server, params.name);
|
|
375
|
-
if (!tool) {
|
|
376
|
-
throw createMcpError(ErrorCode.MethodNotFound, `Unknown tool: ${params.name}`);
|
|
377
|
-
}
|
|
378
|
-
const taskMeta = getTaskMeta(params);
|
|
379
|
-
validateTaskSupport(server, params.name, !!taskMeta);
|
|
380
|
-
const parsed = tryParseArguments(tool, params.arguments);
|
|
381
|
-
if (!parsed.ok)
|
|
382
|
-
return parsed.response;
|
|
383
|
-
if (taskMeta) {
|
|
384
|
-
return enqueueTaskToolExecution(server, tool, params, taskMeta, context, parsed.value);
|
|
385
|
-
}
|
|
386
|
-
const progressState = { closed: false };
|
|
387
|
-
logDebug('Executing task-capable tool inline', {
|
|
388
|
-
tool: params.name,
|
|
389
|
-
hasProgressToken: params._meta?.progressToken !== undefined,
|
|
390
|
-
}, Loggers.LOG_TASKS);
|
|
391
|
-
try {
|
|
392
|
-
return await tool.execute(parsed.value, {
|
|
393
|
-
...buildToolHandlerExtra(context, params._meta),
|
|
394
|
-
progressState,
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
finally {
|
|
398
|
-
progressState.closed = true;
|
|
399
|
-
}
|
|
400
|
-
});
|
|
110
|
+
throw createProtocolError(ProtocolErrorCode.ResourceNotFound, 'Task not found');
|
|
401
111
|
}
|
|
402
112
|
/* -------------------------------------------------------------------------------------------------
|
|
403
113
|
* Task handler schemas and registration
|
|
404
114
|
* ------------------------------------------------------------------------------------------------- */
|
|
405
|
-
const TaskGetSchema = z.looseObject({
|
|
406
|
-
method: z.literal('tasks/get', 'Expected "tasks/get"'),
|
|
407
|
-
params: z.looseObject({ taskId: z.string('Expected string') }, 'Expected object'),
|
|
408
|
-
}, 'Invalid request');
|
|
409
|
-
const TaskListSchema = z.looseObject({
|
|
410
|
-
method: z.literal('tasks/list', 'Expected "tasks/list"'),
|
|
411
|
-
params: z
|
|
412
|
-
.looseObject({
|
|
413
|
-
cursor: z.string('Expected string').optional(),
|
|
414
|
-
}, 'Expected object')
|
|
415
|
-
.optional(),
|
|
416
|
-
}, 'Invalid request');
|
|
417
|
-
const TaskCancelSchema = z.looseObject({
|
|
418
|
-
method: z.literal('tasks/cancel', 'Expected "tasks/cancel"'),
|
|
419
|
-
params: z.looseObject({ taskId: z.string('Expected string') }, 'Expected object'),
|
|
420
|
-
}, 'Invalid request');
|
|
421
115
|
const TaskDeleteSchema = z.looseObject({
|
|
422
116
|
method: z.literal('tasks/delete', 'Expected "tasks/delete"'),
|
|
423
117
|
params: z.looseObject({ taskId: z.string('Expected string') }, 'Expected object'),
|
|
424
118
|
}, 'Invalid request');
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
const parsedExtra = parseHandlerExtra(extra);
|
|
431
|
-
return {
|
|
432
|
-
parsedExtra,
|
|
433
|
-
ownerKey: resolveTaskOwnerKey(parsedExtra),
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
function throwStoredTaskError(task) {
|
|
437
|
-
if (task.error) {
|
|
438
|
-
throw createMcpError(task.error.code, task.error.message, task.error.data);
|
|
439
|
-
}
|
|
440
|
-
throw createMcpError(ErrorCode.InternalError, task.statusMessage ?? 'Execution failed', { taskId: task.taskId });
|
|
441
|
-
}
|
|
442
|
-
export function registerTaskHandlers(server, options) {
|
|
443
|
-
const sdkCallToolHandler = getSdkCallToolHandler(server);
|
|
444
|
-
const taskCapableToolsRegistered = hasRegisteredTaskCapableTools(server);
|
|
445
|
-
const requireInterception = options?.requireInterception ?? true;
|
|
446
|
-
if (!sdkCallToolHandler) {
|
|
447
|
-
if (taskCapableToolsRegistered && requireInterception) {
|
|
448
|
-
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.');
|
|
449
|
-
}
|
|
450
|
-
logWarn('Task call interception disabled: SDK tools/call handler unavailable; task-capable tools require MCP SDK compatibility update', { sdkVersion: 'unknown' }, Loggers.LOG_TASKS);
|
|
451
|
-
}
|
|
452
|
-
if (sdkCallToolHandler) {
|
|
453
|
-
server.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
454
|
-
const parsedExtra = parseHandlerExtra(extra);
|
|
455
|
-
const requestId = parsedExtra?.requestId !== undefined
|
|
456
|
-
? String(parsedExtra.requestId)
|
|
457
|
-
: randomUUID();
|
|
458
|
-
return runWithRequestContext({
|
|
459
|
-
requestId,
|
|
460
|
-
operationId: requestId,
|
|
461
|
-
...(parsedExtra?.sessionId
|
|
462
|
-
? { sessionId: parsedExtra.sessionId }
|
|
463
|
-
: {}),
|
|
464
|
-
}, () => {
|
|
465
|
-
const toolName = request.params.name;
|
|
466
|
-
// Only intercept task-capable tools managed by the local task registry.
|
|
467
|
-
// Delegate all other tools to the SDK handler to avoid shadowing future tools.
|
|
468
|
-
if (!hasTaskCapableTool(server, toolName)) {
|
|
469
|
-
return sdkCallToolHandler(request, extra);
|
|
470
|
-
}
|
|
471
|
-
const parsed = parseExtendedCallToolRequest(request);
|
|
472
|
-
const context = resolveToolCallContext(parsedExtra, parsed.params._meta);
|
|
473
|
-
logDebug('Intercepted task-capable tool call', {
|
|
474
|
-
tool: toolName,
|
|
475
|
-
taskRequested: getTaskMeta(parsed.params) !== undefined,
|
|
476
|
-
hasProgressToken: parsed.params._meta?.progressToken !== undefined,
|
|
477
|
-
}, Loggers.LOG_TASKS);
|
|
478
|
-
return handleToolCallRequest(server, parsed, context);
|
|
479
|
-
});
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
|
-
server.server.setRequestHandler(TaskGetSchema, (request, extra) => {
|
|
483
|
-
const { taskId } = request.params;
|
|
484
|
-
const { ownerKey } = resolveOwnerScopedExtra(extra);
|
|
485
|
-
logDebug('tasks/get requested', { taskId }, Loggers.LOG_TASKS);
|
|
486
|
-
const task = taskManager.getTask(taskId, ownerKey);
|
|
487
|
-
if (!task)
|
|
488
|
-
throwTaskNotFound();
|
|
489
|
-
return withRelatedTaskSummaryMeta(toTaskSummary(task), task.taskId);
|
|
490
|
-
});
|
|
491
|
-
server.server.setRequestHandler(TaskResultSchema, (request, extra) => {
|
|
492
|
-
const { taskId } = request.params;
|
|
493
|
-
const { ownerKey } = resolveOwnerScopedExtra(extra);
|
|
494
|
-
logDebug('tasks/result requested', { taskId }, Loggers.LOG_TASKS);
|
|
495
|
-
const task = taskManager.getTask(taskId, ownerKey);
|
|
496
|
-
if (!task)
|
|
497
|
-
throwTaskNotFound();
|
|
498
|
-
if (task.status === 'submitted' || task.status === 'working') {
|
|
499
|
-
throw createMcpError(ErrorCode.InvalidParams, 'Task result is not available until the task completes', { taskId, status: task.status });
|
|
500
|
-
}
|
|
501
|
-
if (task.status === 'input_required') {
|
|
502
|
-
throw createMcpError(ErrorCode.InvalidParams, 'Task result is not available while the task is waiting for input', { taskId, status: task.status });
|
|
503
|
-
}
|
|
504
|
-
try {
|
|
505
|
-
if (task.status === 'cancelled') {
|
|
506
|
-
throwStoredTaskError(task);
|
|
507
|
-
}
|
|
508
|
-
if (task.status === 'failed') {
|
|
509
|
-
throwStoredTaskError(task);
|
|
510
|
-
}
|
|
511
|
-
const result = isServerResult(task.result)
|
|
512
|
-
? task.result
|
|
513
|
-
: { content: [] };
|
|
514
|
-
return withRelatedTaskMeta(result, task.taskId);
|
|
515
|
-
}
|
|
516
|
-
finally {
|
|
517
|
-
// Shrink keepAlive only after the result has been fully constructed
|
|
518
|
-
// and is about to be delivered — avoids premature expiry if result
|
|
519
|
-
// construction throws.
|
|
520
|
-
taskManager.shrinkKeepAliveAfterDelivery(taskId);
|
|
119
|
+
export function registerTaskHandlers(server) {
|
|
120
|
+
const taskCapableToolsRegistered = config.tools.enabled.includes('fetch-url');
|
|
121
|
+
server.server.fallbackRequestHandler = (request, ctx) => {
|
|
122
|
+
if (request.method !== 'tasks/delete') {
|
|
123
|
+
throw createProtocolError(ProtocolErrorCode.MethodNotFound, `Method not found: ${request.method}`);
|
|
521
124
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
const
|
|
525
|
-
const cursor = request.params?.cursor;
|
|
526
|
-
logDebug('tasks/list requested', { hasCursor: cursor !== undefined }, Loggers.LOG_TASKS);
|
|
527
|
-
const { tasks, nextCursor } = taskManager.listTasks(cursor === undefined ? { ownerKey } : { ownerKey, cursor });
|
|
528
|
-
return {
|
|
529
|
-
tasks: tasks.map((task) => withRelatedTaskSummaryMeta(toTaskSummary(task), task.taskId)),
|
|
530
|
-
nextCursor,
|
|
531
|
-
};
|
|
532
|
-
});
|
|
533
|
-
server.server.setRequestHandler(TaskCancelSchema, (request, extra) => {
|
|
534
|
-
const { taskId } = request.params;
|
|
535
|
-
const { ownerKey } = resolveOwnerScopedExtra(extra);
|
|
536
|
-
logDebug('tasks/cancel requested', { taskId }, Loggers.LOG_TASKS);
|
|
537
|
-
const task = taskManager.cancelTask(taskId, ownerKey);
|
|
538
|
-
if (!task)
|
|
539
|
-
throwTaskNotFound();
|
|
540
|
-
abortTaskExecution(taskId);
|
|
541
|
-
emitTaskStatusNotification(server, task);
|
|
542
|
-
return withRelatedTaskSummaryMeta(toTaskSummary(task), task.taskId);
|
|
543
|
-
});
|
|
544
|
-
server.server.setRequestHandler(TaskDeleteSchema, (request, extra) => {
|
|
545
|
-
const { taskId } = request.params;
|
|
546
|
-
const { ownerKey } = resolveOwnerScopedExtra(extra);
|
|
125
|
+
const parsedRequest = TaskDeleteSchema.parse(request);
|
|
126
|
+
const { taskId } = parsedRequest.params;
|
|
127
|
+
const ownerKey = resolveOwnerKeyFromContext(ctx);
|
|
547
128
|
logDebug('tasks/delete requested', { taskId }, Loggers.LOG_TASKS);
|
|
548
129
|
const deleted = taskManager.deleteTask(taskId, ownerKey);
|
|
549
130
|
if (!deleted)
|
|
550
131
|
throwTaskNotFound();
|
|
551
|
-
return {};
|
|
552
|
-
}
|
|
132
|
+
return Promise.resolve({});
|
|
133
|
+
};
|
|
553
134
|
return {
|
|
554
|
-
interceptedToolsCall: sdkCallToolHandler !== null,
|
|
555
135
|
taskCapableToolsRegistered,
|
|
556
136
|
};
|
|
557
137
|
}
|
|
558
|
-
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
if (obj[key] !== undefined) {
|
|
562
|
-
result[key] = obj[key];
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
return result;
|
|
566
|
-
}
|
|
567
|
-
function normalizeSendNotification(sendNotification) {
|
|
568
|
-
if (typeof sendNotification !== 'function')
|
|
138
|
+
function resolveAuthenticatedSubject(authInfo) {
|
|
139
|
+
const extra = isObject(authInfo?.extra) ? authInfo.extra : undefined;
|
|
140
|
+
if (!extra)
|
|
569
141
|
return undefined;
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
};
|
|
574
|
-
}
|
|
575
|
-
function normalizeAuthInfo(authInfo) {
|
|
576
|
-
if (!isObject(authInfo))
|
|
577
|
-
return undefined;
|
|
578
|
-
const { clientId, token } = authInfo;
|
|
579
|
-
const normalized = {};
|
|
580
|
-
if (typeof clientId === 'string')
|
|
581
|
-
normalized.clientId = clientId;
|
|
582
|
-
if (typeof token === 'string')
|
|
583
|
-
normalized.token = token;
|
|
584
|
-
return normalized.clientId || normalized.token ? normalized : undefined;
|
|
585
|
-
}
|
|
586
|
-
export function parseHandlerExtra(extra) {
|
|
587
|
-
if (!isObject(extra))
|
|
588
|
-
return undefined;
|
|
589
|
-
const parsed = {};
|
|
590
|
-
const { authInfo, signal, requestId, sendNotification } = extra;
|
|
591
|
-
const sessionId = resolveSessionIdFromExtra(extra);
|
|
592
|
-
if (sessionId)
|
|
593
|
-
parsed.sessionId = sessionId;
|
|
594
|
-
const normalizedAuthInfo = normalizeAuthInfo(authInfo);
|
|
595
|
-
if (normalizedAuthInfo) {
|
|
596
|
-
parsed.authInfo = normalizedAuthInfo;
|
|
142
|
+
const { subject, sub } = extra;
|
|
143
|
+
if (typeof subject === 'string' && subject.length > 0) {
|
|
144
|
+
return subject;
|
|
597
145
|
}
|
|
598
|
-
|
|
599
|
-
parsed.signal = signal;
|
|
600
|
-
if (typeof requestId === 'string' || typeof requestId === 'number') {
|
|
601
|
-
parsed.requestId = requestId;
|
|
602
|
-
}
|
|
603
|
-
const normalizedSendNotification = normalizeSendNotification(sendNotification);
|
|
604
|
-
if (normalizedSendNotification) {
|
|
605
|
-
parsed.sendNotification = normalizedSendNotification;
|
|
606
|
-
}
|
|
607
|
-
return parsed;
|
|
146
|
+
return typeof sub === 'string' && sub.length > 0 ? sub : undefined;
|
|
608
147
|
}
|
|
609
148
|
export function buildAuthenticatedOwnerKey(authInfo) {
|
|
149
|
+
const authSubject = resolveAuthenticatedSubject(authInfo);
|
|
150
|
+
if (authSubject) {
|
|
151
|
+
const hashInput = `subject:${authSubject}`;
|
|
152
|
+
return `auth:${hash('sha256', hashInput, 'hex')}`;
|
|
153
|
+
}
|
|
610
154
|
const authClientId = typeof authInfo?.clientId === 'string' ? authInfo.clientId : '';
|
|
611
155
|
const authToken = typeof authInfo?.token === 'string' ? authInfo.token : '';
|
|
612
156
|
if (authClientId || authToken) {
|
|
@@ -615,127 +159,24 @@ export function buildAuthenticatedOwnerKey(authInfo) {
|
|
|
615
159
|
}
|
|
616
160
|
return undefined;
|
|
617
161
|
}
|
|
618
|
-
export function
|
|
619
|
-
const
|
|
620
|
-
if (
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
162
|
+
export function resolveOwnerKeyFromContext(ctx) {
|
|
163
|
+
const authInfo = ctx.http?.authInfo;
|
|
164
|
+
if (authInfo) {
|
|
165
|
+
const identity = {
|
|
166
|
+
clientId: authInfo.clientId,
|
|
167
|
+
token: authInfo.token,
|
|
168
|
+
...(authInfo.extra ? { extra: authInfo.extra } : {}),
|
|
169
|
+
};
|
|
170
|
+
const authenticatedOwnerKey = buildAuthenticatedOwnerKey(identity);
|
|
171
|
+
if (authenticatedOwnerKey)
|
|
172
|
+
return authenticatedOwnerKey;
|
|
173
|
+
}
|
|
174
|
+
const { sessionId } = ctx;
|
|
175
|
+
if (typeof sessionId === 'string' && sessionId.length > 0) {
|
|
176
|
+
return resolveMcpSessionOwnerKey(sessionId) ?? `session:${sessionId}`;
|
|
624
177
|
}
|
|
625
178
|
return 'default';
|
|
626
179
|
}
|
|
627
|
-
function resolveRequestIdFromExtra(extra) {
|
|
628
|
-
if (!isObject(extra))
|
|
629
|
-
return undefined;
|
|
630
|
-
const { requestId } = extra;
|
|
631
|
-
if (typeof requestId === 'string')
|
|
632
|
-
return requestId;
|
|
633
|
-
if (typeof requestId === 'number')
|
|
634
|
-
return String(requestId);
|
|
635
|
-
return undefined;
|
|
636
|
-
}
|
|
637
|
-
function getHeaderString(headers, name) {
|
|
638
|
-
const value = headers[name];
|
|
639
|
-
if (typeof value === 'string')
|
|
640
|
-
return value;
|
|
641
|
-
if (!Array.isArray(value))
|
|
642
|
-
return undefined;
|
|
643
|
-
return value.find((entry) => typeof entry === 'string');
|
|
644
|
-
}
|
|
645
|
-
function resolveSessionIdFromExtra(extra) {
|
|
646
|
-
if (!isObject(extra))
|
|
647
|
-
return undefined;
|
|
648
|
-
const { sessionId } = extra;
|
|
649
|
-
if (typeof sessionId === 'string')
|
|
650
|
-
return sessionId;
|
|
651
|
-
const { requestInfo } = extra;
|
|
652
|
-
if (!isObject(requestInfo))
|
|
653
|
-
return undefined;
|
|
654
|
-
const { headers } = requestInfo;
|
|
655
|
-
if (!isObject(headers))
|
|
656
|
-
return undefined;
|
|
657
|
-
return (getHeaderString(headers, 'mcp-session-id') ??
|
|
658
|
-
getHeaderString(headers, 'x-mcp-session-id'));
|
|
659
|
-
}
|
|
660
|
-
function resolveToolExecutionContext(extra, requestMeta) {
|
|
661
|
-
return compact({
|
|
662
|
-
ownerKey: resolveTaskOwnerKey(extra),
|
|
663
|
-
sessionId: extra?.sessionId,
|
|
664
|
-
signal: extra?.signal,
|
|
665
|
-
requestId: extra?.requestId,
|
|
666
|
-
sendNotification: extra?.sendNotification,
|
|
667
|
-
requestMeta: sanitizeToolCallMeta(requestMeta),
|
|
668
|
-
});
|
|
669
|
-
}
|
|
670
|
-
export function resolveToolCallContext(extra, requestMeta) {
|
|
671
|
-
return resolveToolExecutionContext(extra, requestMeta);
|
|
672
|
-
}
|
|
673
|
-
export function buildToolHandlerExtra(context, requestMeta) {
|
|
674
|
-
return compact({
|
|
675
|
-
signal: context.signal,
|
|
676
|
-
requestId: context.requestId,
|
|
677
|
-
sendNotification: context.sendNotification,
|
|
678
|
-
_meta: sanitizeToolCallMeta(requestMeta ?? context.requestMeta),
|
|
679
|
-
});
|
|
680
|
-
}
|
|
681
|
-
export function withRequestContextIfMissing(handler) {
|
|
682
|
-
return async (params, extra) => {
|
|
683
|
-
const existingRequestId = getRequestId();
|
|
684
|
-
if (existingRequestId) {
|
|
685
|
-
const traceMeta = isObject(extra) && isObject(extra['_meta'])
|
|
686
|
-
? extra['_meta']
|
|
687
|
-
: undefined;
|
|
688
|
-
return runWithTraceContext(traceMeta, () => handler(params, extra));
|
|
689
|
-
}
|
|
690
|
-
const derivedRequestId = resolveRequestIdFromExtra(extra) ?? randomUUID();
|
|
691
|
-
const derivedSessionId = resolveSessionIdFromExtra(extra);
|
|
692
|
-
const traceMeta = isObject(extra) && isObject(extra['_meta']) ? extra['_meta'] : undefined;
|
|
693
|
-
return runWithRequestContext({
|
|
694
|
-
requestId: derivedRequestId,
|
|
695
|
-
operationId: derivedRequestId,
|
|
696
|
-
...(derivedSessionId ? { sessionId: derivedSessionId } : {}),
|
|
697
|
-
}, () => runWithTraceContext(traceMeta, () => handler(params, extra)));
|
|
698
|
-
};
|
|
699
|
-
}
|
|
700
180
|
export function isServerResult(value) {
|
|
701
181
|
return isObject(value) && Array.isArray(value['content']);
|
|
702
182
|
}
|
|
703
|
-
const taskCapableToolsByServer = new WeakMap();
|
|
704
|
-
function getServerToolMap(server) {
|
|
705
|
-
let toolMap = taskCapableToolsByServer.get(server);
|
|
706
|
-
if (toolMap)
|
|
707
|
-
return toolMap;
|
|
708
|
-
toolMap = new Map();
|
|
709
|
-
taskCapableToolsByServer.set(server, toolMap);
|
|
710
|
-
registerServerLifecycleCleanup(server, () => {
|
|
711
|
-
taskCapableToolsByServer.delete(server);
|
|
712
|
-
});
|
|
713
|
-
return toolMap;
|
|
714
|
-
}
|
|
715
|
-
export function registerTaskCapableTool(server, descriptor) {
|
|
716
|
-
getServerToolMap(server).set(descriptor.name, {
|
|
717
|
-
...descriptor,
|
|
718
|
-
taskSupport: descriptor.taskSupport ?? 'optional',
|
|
719
|
-
});
|
|
720
|
-
}
|
|
721
|
-
export function unregisterTaskCapableTool(server, name) {
|
|
722
|
-
getServerToolMap(server).delete(name);
|
|
723
|
-
}
|
|
724
|
-
export function getTaskCapableTool(server, name) {
|
|
725
|
-
return getServerToolMap(server).get(name);
|
|
726
|
-
}
|
|
727
|
-
export function getTaskCapableToolSupport(server, name) {
|
|
728
|
-
return getServerToolMap(server).get(name)?.taskSupport;
|
|
729
|
-
}
|
|
730
|
-
export function hasTaskCapableTool(server, name) {
|
|
731
|
-
return getServerToolMap(server).has(name);
|
|
732
|
-
}
|
|
733
|
-
export function hasRegisteredTaskCapableTools(server) {
|
|
734
|
-
return getServerToolMap(server).size > 0;
|
|
735
|
-
}
|
|
736
|
-
export function setTaskCapableToolSupport(server, name, support) {
|
|
737
|
-
const descriptor = getServerToolMap(server).get(name);
|
|
738
|
-
if (!descriptor)
|
|
739
|
-
return;
|
|
740
|
-
descriptor.taskSupport = support;
|
|
741
|
-
}
|