@apify/actors-mcp-server 0.11.7-beta.1 → 0.11.7-beta.11
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/index_internals.d.ts +7 -12
- package/dist/index_internals.d.ts.map +1 -1
- package/dist/index_internals.js +6 -11
- package/dist/index_internals.js.map +1 -1
- package/dist/mcp/actors.d.ts +0 -4
- package/dist/mcp/actors.d.ts.map +1 -1
- package/dist/mcp/actors.js +0 -13
- package/dist/mcp/actors.js.map +1 -1
- package/dist/mcp/server.d.ts +3 -33
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +103 -645
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/task_execution.d.ts +42 -0
- package/dist/mcp/task_execution.d.ts.map +1 -0
- package/dist/mcp/task_execution.js +310 -0
- package/dist/mcp/task_execution.js.map +1 -0
- package/dist/mcp/tool_call_error_mapper.d.ts +45 -0
- package/dist/mcp/tool_call_error_mapper.d.ts.map +1 -0
- package/dist/mcp/tool_call_error_mapper.js +59 -0
- package/dist/mcp/tool_call_error_mapper.js.map +1 -0
- package/dist/mcp/tool_call_telemetry.d.ts +37 -0
- package/dist/mcp/tool_call_telemetry.d.ts.map +1 -0
- package/dist/mcp/tool_call_telemetry.js +85 -0
- package/dist/mcp/tool_call_telemetry.js.map +1 -0
- package/dist/mcp/tool_dispatch.d.ts +49 -0
- package/dist/mcp/tool_dispatch.d.ts.map +1 -0
- package/dist/mcp/tool_dispatch.js +208 -0
- package/dist/mcp/tool_dispatch.js.map +1 -0
- package/dist/payments/const.d.ts +1 -0
- package/dist/payments/const.d.ts.map +1 -1
- package/dist/payments/const.js +1 -0
- package/dist/payments/const.js.map +1 -1
- package/dist/payments/skyfire.d.ts.map +1 -1
- package/dist/payments/skyfire.js +5 -6
- package/dist/payments/skyfire.js.map +1 -1
- package/dist/tools/actors/call_actor.d.ts +0 -3
- package/dist/tools/actors/call_actor.d.ts.map +1 -1
- package/dist/tools/actors/call_actor.js +2 -11
- package/dist/tools/actors/call_actor.js.map +1 -1
- package/dist/tools/actors/fetch_actor_details.d.ts +1 -7
- package/dist/tools/actors/fetch_actor_details.d.ts.map +1 -1
- package/dist/tools/actors/fetch_actor_details.js +14 -22
- package/dist/tools/actors/fetch_actor_details.js.map +1 -1
- package/dist/tools/actors/search_actors.d.ts +1 -6
- package/dist/tools/actors/search_actors.d.ts.map +1 -1
- package/dist/tools/actors/search_actors.js +17 -24
- package/dist/tools/actors/search_actors.js.map +1 -1
- package/dist/tools/dev/report_problem.d.ts +13 -0
- package/dist/tools/dev/report_problem.d.ts.map +1 -1
- package/dist/tools/dev/report_problem.js +14 -0
- package/dist/tools/dev/report_problem.js.map +1 -1
- package/dist/tools/docs/search_apify_docs.d.ts.map +1 -1
- package/dist/tools/docs/search_apify_docs.js +1 -0
- package/dist/tools/docs/search_apify_docs.js.map +1 -1
- package/dist/tools/runs/get_actor_run.d.ts +1 -6
- package/dist/tools/runs/get_actor_run.d.ts.map +1 -1
- package/dist/tools/runs/get_actor_run.js +17 -24
- package/dist/tools/runs/get_actor_run.js.map +1 -1
- package/dist/tools/storage/get_dataset.d.ts.map +1 -1
- package/dist/tools/storage/get_dataset.js +6 -5
- package/dist/tools/storage/get_dataset.js.map +1 -1
- package/dist/tools/storage/get_dataset_items.d.ts.map +1 -1
- package/dist/tools/storage/get_dataset_items.js +6 -8
- package/dist/tools/storage/get_dataset_items.js.map +1 -1
- package/dist/tools/storage/get_dataset_list.d.ts.map +1 -1
- package/dist/tools/storage/get_dataset_list.js +8 -9
- package/dist/tools/storage/get_dataset_list.js.map +1 -1
- package/dist/tools/storage/get_dataset_schema.d.ts.map +1 -1
- package/dist/tools/storage/get_dataset_schema.js +6 -4
- package/dist/tools/storage/get_dataset_schema.js.map +1 -1
- package/dist/tools/storage/get_key_value_store.js +3 -3
- package/dist/tools/storage/get_key_value_store.js.map +1 -1
- package/dist/tools/storage/get_key_value_store_keys.js +4 -4
- package/dist/tools/storage/get_key_value_store_keys.js.map +1 -1
- package/dist/tools/storage/get_key_value_store_list.d.ts.map +1 -1
- package/dist/tools/storage/get_key_value_store_list.js +8 -9
- package/dist/tools/storage/get_key_value_store_list.js.map +1 -1
- package/dist/tools/storage/get_key_value_store_record.d.ts.map +1 -1
- package/dist/tools/storage/get_key_value_store_record.js +3 -2
- package/dist/tools/storage/get_key_value_store_record.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types.d.ts +0 -11
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/actor_details.d.ts.map +1 -1
- package/dist/utils/actor_details.js +10 -3
- package/dist/utils/actor_details.js.map +1 -1
- package/dist/utils/logging.d.ts +2 -0
- package/dist/utils/logging.d.ts.map +1 -1
- package/dist/utils/logging.js +45 -1
- package/dist/utils/logging.js.map +1 -1
- package/dist/utils/payment_errors.d.ts +7 -1
- package/dist/utils/payment_errors.d.ts.map +1 -1
- package/dist/utils/payment_errors.js +10 -0
- package/dist/utils/payment_errors.js.map +1 -1
- package/dist/web/dist/actor-run-widget.js +0 -3
- package/dist/web/dist/search-actors-widget.js +0 -3
- package/package.json +2 -2
- package/dist/web/dist/actor-detail-widget.js +0 -4551
package/dist/mcp/server.js
CHANGED
|
@@ -8,7 +8,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
8
8
|
import { getUiCapability, RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/server';
|
|
9
9
|
import { InMemoryTaskStore } from '@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js';
|
|
10
10
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
11
|
-
import { CallToolRequestSchema,
|
|
11
|
+
import { CallToolRequestSchema, CancelTaskRequestSchema, ErrorCode, GetPromptRequestSchema, GetTaskPayloadRequestSchema, GetTaskRequestSchema, InitializeRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListTasksRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema, RELATED_TASK_META_KEY, SetLevelRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
12
12
|
import dedent from 'dedent';
|
|
13
13
|
import log from '@apify/log';
|
|
14
14
|
import { parseBooleanOrNull } from '@apify/utilities';
|
|
@@ -19,30 +19,28 @@ import { prompts } from '../prompts/index.js';
|
|
|
19
19
|
import { createResourceService } from '../resources/resource_service.js';
|
|
20
20
|
import { resolveAvailableWidgets } from '../resources/widgets.js';
|
|
21
21
|
import { getServerInfo } from '../server_card.js';
|
|
22
|
-
import {
|
|
22
|
+
import { getTelemetryEnv } from '../telemetry.js';
|
|
23
23
|
import { decodeDotPropertyNames } from '../tools/actor_input_schema.js';
|
|
24
24
|
import { legacyToolNameToNew } from '../tools/actor_tool_naming.js';
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import { appendReportProblemNudge } from '../tools/dev/report_problem.js';
|
|
25
|
+
import { checkPaymentProviderStandbyConflict } from '../tools/actors/call_actor.js';
|
|
26
|
+
import { withReportProblemNudge } from '../tools/dev/report_problem.js';
|
|
28
27
|
import { getActorsAsTools } from '../tools/index.js';
|
|
29
28
|
import { SERVER_MODE, TOOL_TYPE } from '../types.js';
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import { computeToolResponseBytes, getToolCallErrorUserText, respondErrorNoTelemetry, respondOk, } from '../utils/mcp.js';
|
|
29
|
+
import { isMcpClientFaultMessage, logHttpError, sanitizeMezmoMessage } from '../utils/logging.js';
|
|
30
|
+
import { respondOk } from '../utils/mcp.js';
|
|
33
31
|
import { isReportProblemBlockedForClient } from '../utils/mcp_clients.js';
|
|
34
|
-
import { buildPaymentRequiredResponse, isX402PaymentRequiredError } from '../utils/payment_errors.js';
|
|
35
32
|
import { createProgressTracker } from '../utils/progress.js';
|
|
36
33
|
import { getServerInstructions } from '../utils/server-instructions/index.js';
|
|
37
34
|
import { parseServerMode, resolveServerMode } from '../utils/server_mode.js';
|
|
38
|
-
import {
|
|
35
|
+
import { extractAjvErrorDetails } from '../utils/tool_status.js';
|
|
39
36
|
import { buildActorFields, extractActorId, extractActorName, getToolFullName, getToolPublicFieldOnly, } from '../utils/tools.js';
|
|
40
37
|
import { getActors, getToolsForServerMode, toolNamesToInput } from '../utils/tools_loader.js';
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
38
|
+
import { LOG_LEVEL_MAP } from './const.js';
|
|
39
|
+
import { emitTaskStatusNotification, executeToolAndUpdateTask } from './task_execution.js';
|
|
40
|
+
import { buildToolCallErrorResult, TOOL_CALL_ERROR_KIND } from './tool_call_error_mapper.js';
|
|
41
|
+
import { logToolCallAndTelemetry, prepareTelemetryData } from './tool_call_telemetry.js';
|
|
42
|
+
import { dispatchToolCall } from './tool_dispatch.js';
|
|
43
|
+
import { parseInputParamsFromUrl, storeTaskResultOrSkipIfExpired } from './utils.js';
|
|
46
44
|
/**
|
|
47
45
|
* Returns true when the initialize request advertises the MCP Apps UI extension
|
|
48
46
|
* with the widget MIME type. Used to resolve `'auto'` server mode.
|
|
@@ -56,33 +54,6 @@ function isUiSupportedByClient(request) {
|
|
|
56
54
|
const uiCap = getUiCapability((_a = request === null || request === void 0 ? void 0 : request.params) === null || _a === void 0 ? void 0 : _a.capabilities);
|
|
57
55
|
return (_c = (_b = uiCap === null || uiCap === void 0 ? void 0 : uiCap.mimeTypes) === null || _b === void 0 ? void 0 : _b.includes(RESOURCE_MIME_TYPE)) !== null && _c !== void 0 ? _c : false;
|
|
58
56
|
}
|
|
59
|
-
/** Send notifications/tasks/status for taskId. Routes via session transport (no relatedRequestId).
|
|
60
|
-
* Swallows errors — notifications are advisory. */
|
|
61
|
-
export async function emitTaskStatusNotification(taskId, mcpSessionId, taskStore, server) {
|
|
62
|
-
try {
|
|
63
|
-
const task = await taskStore.getTask(taskId, mcpSessionId);
|
|
64
|
-
if (!task)
|
|
65
|
-
return;
|
|
66
|
-
// Per spec: notifications/tasks/status MUST NOT carry _meta.related-task (task ID is in params).
|
|
67
|
-
// Called without options so the notification routes through the session transport,
|
|
68
|
-
// not the request-scoped stream (which closes once the initial { task } response is flushed).
|
|
69
|
-
await server.notification({
|
|
70
|
-
method: 'notifications/tasks/status',
|
|
71
|
-
params: {
|
|
72
|
-
taskId: task.taskId,
|
|
73
|
-
status: task.status,
|
|
74
|
-
createdAt: task.createdAt,
|
|
75
|
-
lastUpdatedAt: task.lastUpdatedAt,
|
|
76
|
-
ttl: task.ttl,
|
|
77
|
-
...(task.statusMessage != null && { statusMessage: task.statusMessage }),
|
|
78
|
-
...(task.pollInterval != null && { pollInterval: task.pollInterval }),
|
|
79
|
-
},
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
catch {
|
|
83
|
-
// Silent fail — notifications are advisory
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
57
|
/**
|
|
87
58
|
* Shared diagnostics for a pre-flight failure (standby-provider conflict or missing/invalid payment
|
|
88
59
|
* signature) — a call outcome already known before any Actor runs. Standby rejection wins over the
|
|
@@ -199,18 +170,18 @@ export class ActorsMcpServer {
|
|
|
199
170
|
writable: true,
|
|
200
171
|
value: []
|
|
201
172
|
});
|
|
202
|
-
// Telemetry configuration (resolved from options and env vars
|
|
173
|
+
// Telemetry configuration (resolved from options and env vars, see setupTelemetry)
|
|
203
174
|
Object.defineProperty(this, "telemetryEnabled", {
|
|
204
175
|
enumerable: true,
|
|
205
176
|
configurable: true,
|
|
206
177
|
writable: true,
|
|
207
|
-
value:
|
|
178
|
+
value: void 0
|
|
208
179
|
});
|
|
209
180
|
Object.defineProperty(this, "telemetryEnv", {
|
|
210
181
|
enumerable: true,
|
|
211
182
|
configurable: true,
|
|
212
183
|
writable: true,
|
|
213
|
-
value:
|
|
184
|
+
value: void 0
|
|
214
185
|
});
|
|
215
186
|
// List of widgets that are ready to be served
|
|
216
187
|
Object.defineProperty(this, "availableWidgets", {
|
|
@@ -277,7 +248,9 @@ export class ActorsMcpServer {
|
|
|
277
248
|
},
|
|
278
249
|
instructions: getServerInstructions(),
|
|
279
250
|
});
|
|
280
|
-
this.setupTelemetry();
|
|
251
|
+
const { telemetryEnabled, telemetryEnv } = this.setupTelemetry();
|
|
252
|
+
this.telemetryEnabled = telemetryEnabled;
|
|
253
|
+
this.telemetryEnv = telemetryEnv;
|
|
281
254
|
this.setupInitializeHandler();
|
|
282
255
|
this.setupLoggingProxy();
|
|
283
256
|
this.tools = new Map();
|
|
@@ -296,18 +269,21 @@ export class ActorsMcpServer {
|
|
|
296
269
|
*/
|
|
297
270
|
setupTelemetry() {
|
|
298
271
|
var _a, _b, _c;
|
|
272
|
+
let telemetryEnabled;
|
|
299
273
|
const explicitEnabled = parseBooleanOrNull((_a = this.options.telemetry) === null || _a === void 0 ? void 0 : _a.enabled);
|
|
300
274
|
if (explicitEnabled !== null) {
|
|
301
|
-
|
|
275
|
+
telemetryEnabled = explicitEnabled;
|
|
302
276
|
}
|
|
303
277
|
else {
|
|
304
278
|
const envEnabled = parseBooleanOrNull(process.env.TELEMETRY_ENABLED);
|
|
305
|
-
|
|
279
|
+
telemetryEnabled = envEnabled !== null && envEnabled !== void 0 ? envEnabled : DEFAULT_TELEMETRY_ENABLED;
|
|
306
280
|
}
|
|
307
281
|
// Configure telemetryEnv: explicit option > env var > default ('PROD')
|
|
308
|
-
|
|
309
|
-
|
|
282
|
+
let telemetryEnv = DEFAULT_TELEMETRY_ENV;
|
|
283
|
+
if (telemetryEnabled) {
|
|
284
|
+
telemetryEnv = getTelemetryEnv((_c = (_b = this.options.telemetry) === null || _b === void 0 ? void 0 : _b.env) !== null && _c !== void 0 ? _c : process.env.TELEMETRY_ENV);
|
|
310
285
|
}
|
|
286
|
+
return { telemetryEnabled, telemetryEnv };
|
|
311
287
|
}
|
|
312
288
|
/**
|
|
313
289
|
* Override the SDK's `initialize` request handler to run mode resolution and
|
|
@@ -389,23 +365,10 @@ export class ActorsMcpServer {
|
|
|
389
365
|
* survive a load that never sees an initialize.
|
|
390
366
|
*/
|
|
391
367
|
isReportProblemServable() {
|
|
392
|
-
return (
|
|
368
|
+
return (this.telemetryEnabled &&
|
|
393
369
|
this.clientKnown &&
|
|
394
370
|
!isReportProblemBlockedForClient(this.options.initializeRequestData));
|
|
395
371
|
}
|
|
396
|
-
/**
|
|
397
|
-
* Append the report-problem nudge to a failed tool result. Thin wrapper over
|
|
398
|
-
* {@link appendReportProblemNudge} that fills in whether report-problem is served on this
|
|
399
|
-
* connection; suppression by category/402 is handled inside the helper.
|
|
400
|
-
*/
|
|
401
|
-
withReportProblemNudge(result, failingToolName, failureCategory, failureHttpStatus) {
|
|
402
|
-
return appendReportProblemNudge(result, {
|
|
403
|
-
failingToolName,
|
|
404
|
-
available: this.tools.has(HELPER_TOOLS.PROBLEM_REPORT),
|
|
405
|
-
failureCategory,
|
|
406
|
-
failureHttpStatus,
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
372
|
composePendingToolsForClient() {
|
|
410
373
|
if (this.pendingToolsUntilClientKnown.length === 0)
|
|
411
374
|
return;
|
|
@@ -843,7 +806,7 @@ export class ActorsMcpServer {
|
|
|
843
806
|
* @throws {McpError} - based on the McpServer class code from the typescript MCP SDK
|
|
844
807
|
*/
|
|
845
808
|
this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
846
|
-
var _a, _b, _c, _d, _e
|
|
809
|
+
var _a, _b, _c, _d, _e;
|
|
847
810
|
const params = request.params;
|
|
848
811
|
// eslint-disable-next-line prefer-const
|
|
849
812
|
let { name, arguments: args, _meta: meta } = params;
|
|
@@ -870,7 +833,13 @@ export class ActorsMcpServer {
|
|
|
870
833
|
// On a failed result, nudge the agent to report the blocker via report-problem at the
|
|
871
834
|
// moment it decides what to do next. Gated on the tool actually being served (see
|
|
872
835
|
// isReportProblemServable), so clients where it is blocklisted or telemetry is off never see it.
|
|
873
|
-
const augmented =
|
|
836
|
+
const augmented = withReportProblemNudge({
|
|
837
|
+
result: r,
|
|
838
|
+
tools: this.tools,
|
|
839
|
+
failingToolName: resolvedToolName,
|
|
840
|
+
failureCategory: callDiagnostics.failure_category,
|
|
841
|
+
failureHttpStatus: callDiagnostics.failure_http_status,
|
|
842
|
+
});
|
|
874
843
|
toolResult = augmented;
|
|
875
844
|
return augmented;
|
|
876
845
|
};
|
|
@@ -892,7 +861,12 @@ export class ActorsMcpServer {
|
|
|
892
861
|
};
|
|
893
862
|
// Initialize telemetry with raw tool name — updated below once the tool is resolved.
|
|
894
863
|
// This ensures telemetry is available even for early failures (missing token, tool not found).
|
|
895
|
-
const { telemetryData, userId } = await
|
|
864
|
+
const { telemetryData, userId } = await prepareTelemetryData({
|
|
865
|
+
toolName: name,
|
|
866
|
+
mcpSessionId,
|
|
867
|
+
apifyToken,
|
|
868
|
+
apifyMcpServer: this,
|
|
869
|
+
});
|
|
896
870
|
// actorName/actorId are declared here so they're available in the catch block for telemetry.
|
|
897
871
|
// Set after tool resolution (inside the try block).
|
|
898
872
|
let actorName;
|
|
@@ -1060,7 +1034,7 @@ export class ActorsMcpServer {
|
|
|
1060
1034
|
}
|
|
1061
1035
|
// Execute the tool asynchronously and update task status
|
|
1062
1036
|
setImmediate(() => {
|
|
1063
|
-
|
|
1037
|
+
executeToolAndUpdateTask({
|
|
1064
1038
|
taskId: task.taskId,
|
|
1065
1039
|
tool,
|
|
1066
1040
|
toolArgs: toolArgs,
|
|
@@ -1072,6 +1046,7 @@ export class ActorsMcpServer {
|
|
|
1072
1046
|
mcpSessionId,
|
|
1073
1047
|
actorName,
|
|
1074
1048
|
actorId,
|
|
1049
|
+
apifyMcpServer: this,
|
|
1075
1050
|
}).catch((error) =>
|
|
1076
1051
|
// Benign task-expiry is handled in-method (see the catch block and
|
|
1077
1052
|
// storeTaskResultOrSkipIfExpired); anything reaching here is genuinely unexpected.
|
|
@@ -1090,196 +1065,75 @@ export class ActorsMcpServer {
|
|
|
1090
1065
|
callDiagnostics = outcome.callDiagnostics;
|
|
1091
1066
|
return captureResult(outcome.result);
|
|
1092
1067
|
}
|
|
1093
|
-
//
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
if (tool.type === TOOL_TYPE.ACTOR_MCP) {
|
|
1122
|
-
let client = null;
|
|
1123
|
-
try {
|
|
1124
|
-
client = await connectMCPClient(tool.serverUrl, apifyToken, mcpSessionId);
|
|
1125
|
-
if (!client) {
|
|
1126
|
-
const msg = dedent `
|
|
1127
|
-
Failed to connect to MCP server at "${tool.serverUrl}".
|
|
1128
|
-
Please verify the server URL is correct and accessible, and ensure you have a valid Apify token with appropriate permissions.
|
|
1129
|
-
`;
|
|
1130
|
-
log.softFail(msg, { mcpSessionId, failureCategory: FAILURE_CATEGORY.INTERNAL_ERROR });
|
|
1131
|
-
await this.server.sendLoggingMessage({ level: 'error', data: msg });
|
|
1132
|
-
toolStatus = TOOL_STATUS.SOFT_FAIL;
|
|
1133
|
-
callDiagnostics = { ...callDiagnostics, failure_category: FAILURE_CATEGORY.INTERNAL_ERROR };
|
|
1134
|
-
return captureResult(respondErrorNoTelemetry(msg));
|
|
1135
|
-
}
|
|
1136
|
-
// Only set up notification handlers if progressToken is provided by the client
|
|
1137
|
-
if (progressToken !== undefined && progressToken !== null) {
|
|
1138
|
-
// Set up notification handlers for the client
|
|
1139
|
-
for (const schema of ServerNotificationSchema.options) {
|
|
1140
|
-
const method = schema.shape.method.value;
|
|
1141
|
-
// Forward notifications from the proxy client to the server
|
|
1142
|
-
client.setNotificationHandler(schema, async (notification) => {
|
|
1143
|
-
log.debug('Sending MCP notification', {
|
|
1144
|
-
method,
|
|
1145
|
-
mcpSessionId,
|
|
1146
|
-
notification,
|
|
1147
|
-
});
|
|
1148
|
-
await extra.sendNotification(notification);
|
|
1149
|
-
});
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
log.info('Calling Actor-MCP', {
|
|
1153
|
-
toolName: tool.name,
|
|
1154
|
-
actorMcpToolName: tool.originToolName,
|
|
1155
|
-
actorId: tool.actorId,
|
|
1156
|
-
mcpSessionId,
|
|
1157
|
-
input: logSafeArgs,
|
|
1158
|
-
});
|
|
1159
|
-
const res = await client.callTool({
|
|
1160
|
-
name: tool.originToolName,
|
|
1161
|
-
arguments: toolArgs,
|
|
1162
|
-
_meta: { progressToken },
|
|
1163
|
-
}, CallToolResultSchema, {
|
|
1164
|
-
timeout: EXTERNAL_TOOL_CALL_TIMEOUT_MSEC,
|
|
1165
|
-
});
|
|
1166
|
-
// TODO: actor-mcp responses are opaque — isError could be a user input problem
|
|
1167
|
-
// (e.g. invalid query) or a genuine server failure. We can't distinguish without
|
|
1168
|
-
// parsing the error text. Defaulting to INTERNAL_ERROR for now; revisit when
|
|
1169
|
-
// actor-mcp gets deeper telemetry treatment.
|
|
1170
|
-
if ('isError' in res && res.isError) {
|
|
1171
|
-
toolStatus = TOOL_STATUS.SOFT_FAIL;
|
|
1172
|
-
callDiagnostics = {
|
|
1173
|
-
failure_category: FAILURE_CATEGORY.INTERNAL_ERROR,
|
|
1174
|
-
...buildActorFields(actorName, actorId),
|
|
1175
|
-
};
|
|
1176
|
-
}
|
|
1177
|
-
return captureResult({ ...res });
|
|
1178
|
-
}
|
|
1179
|
-
catch (error) {
|
|
1180
|
-
toolStatus = getToolStatusFromError(error, Boolean((_e = extra.signal) === null || _e === void 0 ? void 0 : _e.aborted));
|
|
1181
|
-
const failureDetail = error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200);
|
|
1182
|
-
callDiagnostics = {
|
|
1183
|
-
failure_category: classifyFailureCategory(error),
|
|
1184
|
-
failure_detail: failureDetail,
|
|
1185
|
-
...buildActorFields(actorName, actorId),
|
|
1186
|
-
};
|
|
1187
|
-
logHttpError(error, `Failed to call MCP tool '${tool.originToolName}' on Actor '${tool.actorId}'`, {
|
|
1188
|
-
actorId: tool.actorId,
|
|
1189
|
-
toolName: tool.originToolName,
|
|
1190
|
-
failureCategory: callDiagnostics.failure_category,
|
|
1191
|
-
});
|
|
1192
|
-
return captureResult(respondErrorNoTelemetry(`Failed to call MCP tool '${tool.originToolName}' on Actor '${tool.actorId}': ${remoteMcpFailureDetail(error)}`));
|
|
1193
|
-
}
|
|
1194
|
-
finally {
|
|
1195
|
-
if (client)
|
|
1196
|
-
await client.close();
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
// Handle actor tool
|
|
1200
|
-
if (tool.type === TOOL_TYPE.ACTOR) {
|
|
1201
|
-
const progressTracker = createProgressTracker(progressToken, extra.sendNotification);
|
|
1202
|
-
try {
|
|
1203
|
-
log.info('Calling Actor', {
|
|
1204
|
-
toolName: tool.name,
|
|
1205
|
-
actorName: tool.actorFullName,
|
|
1206
|
-
mcpSessionId,
|
|
1207
|
-
input: logSafeArgs,
|
|
1208
|
-
});
|
|
1209
|
-
const executorResult = await actorExecutor.executeActorTool({
|
|
1210
|
-
actorFullName: tool.actorFullName,
|
|
1211
|
-
input: toolArgs,
|
|
1212
|
-
apifyClient: apifyClient,
|
|
1213
|
-
callOptions: { memory: tool.memoryMbytes },
|
|
1214
|
-
progressTracker,
|
|
1215
|
-
abortSignal: extra.signal,
|
|
1216
|
-
mcpSessionId,
|
|
1217
|
-
datasetItemsSchema: tool.datasetItemsSchema,
|
|
1218
|
-
});
|
|
1219
|
-
if (!executorResult) {
|
|
1220
|
-
toolStatus = TOOL_STATUS.ABORTED;
|
|
1221
|
-
// Receivers of cancellation notifications SHOULD NOT send a response for the cancelled request
|
|
1222
|
-
// https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/cancellation#behavior-requirements
|
|
1223
|
-
return captureResult({});
|
|
1224
|
-
}
|
|
1225
|
-
// Mirror the INTERNAL branch: read the telemetry the executor embedded on error
|
|
1226
|
-
// results (e.g. respondUserError → SOFT_FAIL/INVALID_INPUT), strip it from the wire,
|
|
1227
|
-
// and set failure_category so the report-problem nudge picks the softer variant.
|
|
1228
|
-
({ toolStatus, callDiagnostics } = applyToolTelemetry(executorResult, actorName, actorId, callDiagnostics));
|
|
1229
|
-
return captureResult(executorResult);
|
|
1230
|
-
}
|
|
1231
|
-
finally {
|
|
1232
|
-
if (progressTracker) {
|
|
1233
|
-
progressTracker.stop();
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
// If we reached here without returning, it means the tool type was not recognized (user error)
|
|
1238
|
-
toolStatus = TOOL_STATUS.SOFT_FAIL;
|
|
1068
|
+
// Progress tracker: opt in for the two INTERNAL tools that emit during a sync wait
|
|
1069
|
+
// (call-actor start+waitForFinish, get-actor-run when waitSecs > 0), and unconditionally
|
|
1070
|
+
// for ACTOR tools. ACTOR_MCP forwards notifications directly, not via a tracker.
|
|
1071
|
+
const progressTrackerOptIn = tool.type === TOOL_TYPE.ACTOR ||
|
|
1072
|
+
(tool.type === TOOL_TYPE.INTERNAL &&
|
|
1073
|
+
(tool.name === HELPER_TOOLS.ACTOR_CALL || tool.name === HELPER_TOOLS.ACTOR_RUNS_GET));
|
|
1074
|
+
const progressTracker = progressTrackerOptIn
|
|
1075
|
+
? createProgressTracker(progressToken, extra.sendNotification)
|
|
1076
|
+
: null;
|
|
1077
|
+
const dispatchResult = await dispatchToolCall({
|
|
1078
|
+
tool,
|
|
1079
|
+
toolArgs: toolArgs,
|
|
1080
|
+
logSafeArgs,
|
|
1081
|
+
apifyToken,
|
|
1082
|
+
apifyClient: apifyClient,
|
|
1083
|
+
apifyMcpServer: this,
|
|
1084
|
+
mcpSessionId,
|
|
1085
|
+
progressToken,
|
|
1086
|
+
progressTracker,
|
|
1087
|
+
shouldForwardNotifications: true,
|
|
1088
|
+
extra,
|
|
1089
|
+
actorName,
|
|
1090
|
+
actorId,
|
|
1091
|
+
taskMode: false,
|
|
1092
|
+
});
|
|
1093
|
+
toolStatus = dispatchResult.toolStatus;
|
|
1094
|
+
callDiagnostics = dispatchResult.callDiagnostics;
|
|
1095
|
+
return captureResult(dispatchResult.result);
|
|
1239
1096
|
}
|
|
1240
1097
|
catch (error) {
|
|
1241
|
-
|
|
1098
|
+
// Re-throw MCP protocol errors (e.g. from failInvalidParams) so the SDK
|
|
1099
|
+
// returns them as JSON-RPC errors. failInvalidParams already set callDiagnostics
|
|
1100
|
+
// with the correct semantic category (e.g. AUTH), so we must not overwrite it.
|
|
1101
|
+
// Re-throwing first is order-equivalent only because no McpError with an HTTP-range
|
|
1102
|
+
// code reaches this catch: an McpError(402) WOULD satisfy the x402 predicate
|
|
1103
|
+
// (getHttpStatusCode falls through to `.code`), but every remote-McpError route is
|
|
1104
|
+
// sealed by an inner catch (ACTOR_MCP branch, call-actor MCP passthrough) and all
|
|
1105
|
+
// in-repo McpErrors use negative ErrorCode.* values.
|
|
1106
|
+
if (error instanceof McpError) {
|
|
1107
|
+
throw error;
|
|
1108
|
+
}
|
|
1109
|
+
const errorResult = buildToolCallErrorResult(error, {
|
|
1110
|
+
toolName: name,
|
|
1111
|
+
actorName,
|
|
1112
|
+
actorId,
|
|
1113
|
+
isAborted: Boolean((_e = extra.signal) === null || _e === void 0 ? void 0 : _e.aborted),
|
|
1114
|
+
});
|
|
1115
|
+
toolStatus = errorResult.toolStatus;
|
|
1242
1116
|
// Propagate 402 Payment Required as a tool result per x402 MCP transport spec:
|
|
1243
|
-
// content[0].text (JSON) + isError: true.
|
|
1244
|
-
//
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
callDiagnostics =
|
|
1248
|
-
|
|
1249
|
-
failure_http_status: 402,
|
|
1250
|
-
...buildActorFields(actorName, actorId),
|
|
1251
|
-
};
|
|
1252
|
-
return captureResult(buildPaymentRequiredResponse(error));
|
|
1117
|
+
// content[0].text (JSON) + isError: true. No log here (unlike the task path). The
|
|
1118
|
+
// concurrent-run limit also surfaces as 402 but is excluded by the predicate and
|
|
1119
|
+
// falls through to the generic run-limit handling below.
|
|
1120
|
+
if (errorResult.kind === TOOL_CALL_ERROR_KIND.PAYMENT) {
|
|
1121
|
+
callDiagnostics = errorResult.callDiagnostics;
|
|
1122
|
+
return captureResult(errorResult.response);
|
|
1253
1123
|
}
|
|
1254
|
-
if (
|
|
1255
|
-
|
|
1256
|
-
callDiagnostics = {
|
|
1257
|
-
failure_category: FAILURE_CATEGORY.PERMISSION_APPROVAL_REQUIRED,
|
|
1258
|
-
failure_http_status: error.statusCode,
|
|
1259
|
-
...buildActorFields(actorName, actorId),
|
|
1260
|
-
};
|
|
1124
|
+
if (errorResult.kind === TOOL_CALL_ERROR_KIND.APPROVAL) {
|
|
1125
|
+
callDiagnostics = errorResult.callDiagnostics;
|
|
1261
1126
|
logHttpError(error, 'Permission approval required while calling tool', {
|
|
1262
1127
|
toolName: name,
|
|
1263
1128
|
mcpSessionId,
|
|
1264
1129
|
});
|
|
1265
|
-
return captureResult(
|
|
1266
|
-
}
|
|
1267
|
-
// Re-throw MCP protocol errors (e.g. from failInvalidParams) so the SDK
|
|
1268
|
-
// returns them as JSON-RPC errors. failInvalidParams already set callDiagnostics
|
|
1269
|
-
// with the correct semantic category (e.g. AUTH), so we must not overwrite it.
|
|
1270
|
-
if (error instanceof McpError) {
|
|
1271
|
-
throw error;
|
|
1130
|
+
return captureResult(errorResult.response);
|
|
1272
1131
|
}
|
|
1273
|
-
toolStatus = getToolStatusFromError(error, Boolean((_f = extra.signal) === null || _f === void 0 ? void 0 : _f.aborted));
|
|
1274
|
-
const failureDetail = error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200);
|
|
1275
1132
|
callDiagnostics = {
|
|
1276
1133
|
// Spread existing diagnostics first (e.g. validation_keyword from failInvalidParams),
|
|
1277
|
-
// then overwrite with freshly computed fields so they take precedence.
|
|
1134
|
+
// then overwrite with the mapper's freshly computed fields so they take precedence.
|
|
1278
1135
|
...callDiagnostics,
|
|
1279
|
-
|
|
1280
|
-
...(httpStatus !== undefined ? { failure_http_status: httpStatus } : {}),
|
|
1281
|
-
failure_detail: failureDetail,
|
|
1282
|
-
...buildActorFields(actorName, actorId),
|
|
1136
|
+
...errorResult.callDiagnostics,
|
|
1283
1137
|
};
|
|
1284
1138
|
logHttpError(error, 'Error occurred while calling tool', {
|
|
1285
1139
|
toolName: name,
|
|
@@ -1298,14 +1152,14 @@ export class ActorsMcpServer {
|
|
|
1298
1152
|
// reuse the local ABORTED-aware toolStatus, do NOT re-derive from the error (which
|
|
1299
1153
|
// would drop ABORTED and leak failureCategory/failureHttpStatus onto the wire).
|
|
1300
1154
|
return captureResult({
|
|
1301
|
-
...respondOk(
|
|
1155
|
+
...respondOk(errorResult.userText),
|
|
1302
1156
|
isError: true,
|
|
1303
1157
|
toolTelemetry: { toolStatus },
|
|
1304
1158
|
});
|
|
1305
1159
|
}
|
|
1306
1160
|
finally {
|
|
1307
1161
|
if (shouldTrackTelemetry) {
|
|
1308
|
-
|
|
1162
|
+
logToolCallAndTelemetry({
|
|
1309
1163
|
toolName: resolvedToolName,
|
|
1310
1164
|
mcpSessionId,
|
|
1311
1165
|
toolStatus,
|
|
@@ -1316,407 +1170,11 @@ export class ActorsMcpServer {
|
|
|
1316
1170
|
args,
|
|
1317
1171
|
result: toolResult,
|
|
1318
1172
|
taskId: preflightTaskId,
|
|
1319
|
-
});
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1322
|
-
const availableTools = this.listToolNames();
|
|
1323
|
-
const msg = dedent `
|
|
1324
|
-
Unknown tool type for "${name}".
|
|
1325
|
-
Available tools: ${availableTools.length > 0 ? availableTools.join(', ') : 'none'}.
|
|
1326
|
-
Please verify the tool name and ensure the tool is properly registered.
|
|
1327
|
-
`;
|
|
1328
|
-
log.softFail(msg, { mcpSessionId, statusCode: 404 });
|
|
1329
|
-
await this.server.sendLoggingMessage({
|
|
1330
|
-
level: 'error',
|
|
1331
|
-
data: msg,
|
|
1332
|
-
});
|
|
1333
|
-
throw new McpError(ErrorCode.InvalidParams, msg);
|
|
1334
|
-
});
|
|
1335
|
-
}
|
|
1336
|
-
/**
|
|
1337
|
-
* Logs tool call completion at INFO level and tracks telemetry.
|
|
1338
|
-
* Computes duration once so both the log line and telemetry event use the same value.
|
|
1339
|
-
* Response bytes and resource ids are derived here from the raw `result` (+ `args`) so every
|
|
1340
|
-
* call site stays a plain hand-off — no path can forget to compute or strip them.
|
|
1341
|
-
*/
|
|
1342
|
-
logToolCallAndTelemetry(params) {
|
|
1343
|
-
const durationMs = Date.now() - params.startTime;
|
|
1344
|
-
// `result` is undefined only on short-circuit paths that never produced a payload (e.g. a
|
|
1345
|
-
// cancelled task); skip byte accounting there. `null`/`{}` still measure as zero bytes.
|
|
1346
|
-
const responseBytes = params.result === undefined ? undefined : computeToolResponseBytes(params.result);
|
|
1347
|
-
log.info('Tool call completed', {
|
|
1348
|
-
toolName: params.toolName,
|
|
1349
|
-
mcpSessionId: params.mcpSessionId,
|
|
1350
|
-
toolStatus: params.toolStatus,
|
|
1351
|
-
durationMs,
|
|
1352
|
-
...(responseBytes !== undefined && {
|
|
1353
|
-
responseContentBytes: responseBytes.contentBytes,
|
|
1354
|
-
responseStructuredContentBytes: responseBytes.structuredContentBytes,
|
|
1355
|
-
responseFileBytes: responseBytes.fileBytes,
|
|
1356
|
-
}),
|
|
1357
|
-
...(params.taskId !== undefined && { taskId: params.taskId }),
|
|
1358
|
-
});
|
|
1359
|
-
if (params.telemetryData) {
|
|
1360
|
-
const finalizedTelemetryData = {
|
|
1361
|
-
...params.telemetryData,
|
|
1362
|
-
tool_status: params.toolStatus,
|
|
1363
|
-
tool_exec_time_ms: durationMs,
|
|
1364
|
-
...(responseBytes && {
|
|
1365
|
-
tool_response_content_bytes: responseBytes.contentBytes,
|
|
1366
|
-
tool_response_structured_content_bytes: responseBytes.structuredContentBytes,
|
|
1367
|
-
tool_response_file_bytes: responseBytes.fileBytes,
|
|
1368
|
-
}),
|
|
1369
|
-
// Always include actor_name/actor_id; failure-specific fields are only present when callDiagnostics has them.
|
|
1370
|
-
...params.callDiagnostics,
|
|
1371
|
-
// Resource ids are read once here from the args + the tool's public output; no tool
|
|
1372
|
-
// threads them back. Applied uniformly, last. See deriveResourceIds.
|
|
1373
|
-
...deriveResourceIds(params.args, params.result),
|
|
1374
|
-
};
|
|
1375
|
-
trackToolCall(params.userId, this.telemetryEnv, finalizedTelemetryData);
|
|
1376
|
-
// A successful report-problem call also emits a dedicated feedback event carrying the
|
|
1377
|
-
// submission. A downstream Segment destination fans it out to Slack/GitHub.
|
|
1378
|
-
if (params.toolName === HELPER_TOOLS.PROBLEM_REPORT &&
|
|
1379
|
-
params.toolStatus === TOOL_STATUS.SUCCEEDED &&
|
|
1380
|
-
params.args) {
|
|
1381
|
-
trackReportedProblem(params.userId, this.telemetryEnv, buildReportedProblemProperties(finalizedTelemetryData, params.args));
|
|
1382
|
-
}
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
// TODO: this function quite duplicates the main tool call login the CallToolRequestSchema handler, we should refactor
|
|
1386
|
-
/**
|
|
1387
|
-
* Executes a tool asynchronously for a long-running task and updates task status.
|
|
1388
|
-
*
|
|
1389
|
-
* @param params - Tool execution parameters
|
|
1390
|
-
* @param params.taskId - The task identifier
|
|
1391
|
-
* @param params.tool - The tool to execute
|
|
1392
|
-
* @param params.args - Tool arguments
|
|
1393
|
-
* @param params.apifyToken - Apify API token
|
|
1394
|
-
* @param params.progressToken - Progress token for notifications
|
|
1395
|
-
* @param params.extra - Extra request handler context
|
|
1396
|
-
* @param params.mcpSessionId - MCP session ID for telemetry
|
|
1397
|
-
*/
|
|
1398
|
-
async executeToolAndUpdateTask(params) {
|
|
1399
|
-
const { taskId, tool, toolArgs, logSafeArgs, apifyClient, apifyToken, progressToken, extra, mcpSessionId, actorName, actorId, } = params;
|
|
1400
|
-
let toolStatus = TOOL_STATUS.SUCCEEDED;
|
|
1401
|
-
// Always populate actor fields so they're tracked on both success and failure paths.
|
|
1402
|
-
let callDiagnostics = { ...buildActorFields(actorName, actorId) };
|
|
1403
|
-
const startTime = Date.now();
|
|
1404
|
-
log.debug('[executeToolAndUpdateTask] Starting task execution', {
|
|
1405
|
-
taskId,
|
|
1406
|
-
toolName: tool.name,
|
|
1407
|
-
mcpSessionId,
|
|
1408
|
-
});
|
|
1409
|
-
// Prepare telemetry before try-catch so it's accessible to both paths.
|
|
1410
|
-
// This avoids re-fetching user data in the error handler.
|
|
1411
|
-
const { telemetryData, userId } = await this.prepareTelemetryData(getToolFullName(tool), mcpSessionId, apifyToken);
|
|
1412
|
-
const finishTaskTracking = (status, diagnostics, result) => {
|
|
1413
|
-
this.logToolCallAndTelemetry({
|
|
1414
|
-
toolName: tool.name,
|
|
1415
|
-
mcpSessionId,
|
|
1416
|
-
toolStatus: status,
|
|
1417
|
-
startTime,
|
|
1418
|
-
taskId,
|
|
1419
|
-
telemetryData,
|
|
1420
|
-
userId,
|
|
1421
|
-
callDiagnostics: diagnostics,
|
|
1422
|
-
args: toolArgs,
|
|
1423
|
-
result,
|
|
1424
|
-
});
|
|
1425
|
-
};
|
|
1426
|
-
// Once a task is cancelled the spec forbids writing a result; every storage path
|
|
1427
|
-
// must short-circuit here. `logSuffix` is concatenated after "Task was cancelled"
|
|
1428
|
-
// so we keep the existing log format and the existing telemetry status per path.
|
|
1429
|
-
const skipIfTaskCancelled = async (logSuffix, status, diagnostics) => {
|
|
1430
|
-
if (!(await isTaskCancelled(taskId, mcpSessionId, this.taskStore)))
|
|
1431
|
-
return false;
|
|
1432
|
-
log.debug(`[executeToolAndUpdateTask] Task was cancelled${logSuffix}`, { taskId, mcpSessionId });
|
|
1433
|
-
finishTaskTracking(status, diagnostics);
|
|
1434
|
-
return true;
|
|
1435
|
-
};
|
|
1436
|
-
// Bridges MCP `tasks/cancel` to the running handler: when the client
|
|
1437
|
-
// explicitly cancels the task, this signal aborts so the underlying
|
|
1438
|
-
// Actor run stops instead of consuming compute until natural completion.
|
|
1439
|
-
// Per MCP tasks spec, request-level aborts (client disconnect,
|
|
1440
|
-
// notifications/cancelled for the original request ID) MUST NOT cancel
|
|
1441
|
-
// the task — `extra.signal` is intentionally not chained here.
|
|
1442
|
-
const cancelWatcher = createTaskCancellationWatcher({
|
|
1443
|
-
taskId,
|
|
1444
|
-
mcpSessionId,
|
|
1445
|
-
taskStore: this.taskStore,
|
|
1446
|
-
});
|
|
1447
|
-
const taskExtra = { ...extra, signal: cancelWatcher.signal };
|
|
1448
|
-
try {
|
|
1449
|
-
log.debug('[executeToolAndUpdateTask] Updating task status to working', {
|
|
1450
|
-
taskId,
|
|
1451
|
-
mcpSessionId,
|
|
1452
|
-
});
|
|
1453
|
-
// The store rejects terminal → 'working' transitions. If `tasks/cancel` raced
|
|
1454
|
-
// with us (between handler dispatch and the first watcher tick at ~500 ms),
|
|
1455
|
-
// updateTaskStatus throws — re-check the store to tell a clean cancel-race
|
|
1456
|
-
// apart from a genuine store error.
|
|
1457
|
-
// noinspection ExceptionCaughtLocallyJS
|
|
1458
|
-
try {
|
|
1459
|
-
await this.taskStore.updateTaskStatus(taskId, 'working', undefined, mcpSessionId);
|
|
1460
|
-
}
|
|
1461
|
-
catch (err) {
|
|
1462
|
-
if (await skipIfTaskCancelled(' before execution started, skipping', TOOL_STATUS.ABORTED, {
|
|
1463
|
-
...buildActorFields(actorName, actorId),
|
|
1464
|
-
}))
|
|
1465
|
-
return;
|
|
1466
|
-
throw err;
|
|
1467
|
-
}
|
|
1468
|
-
await emitTaskStatusNotification(taskId, mcpSessionId, this.taskStore, this.server);
|
|
1469
|
-
// Execute the tool and get the result
|
|
1470
|
-
let result = {};
|
|
1471
|
-
// Callback to propagate Actor run statusMessage into the task store and emit a push notification.
|
|
1472
|
-
// TODO(TC-3): cancel arriving while this is scheduled throws cancelled → working;
|
|
1473
|
-
// currently swallowed by progress.ts's tick catch — guard or catch explicitly.
|
|
1474
|
-
const onStatusMessage = async (message) => {
|
|
1475
|
-
await this.taskStore.updateTaskStatus(taskId, 'working', message, mcpSessionId);
|
|
1476
|
-
await emitTaskStatusNotification(taskId, mcpSessionId, this.taskStore, this.server);
|
|
1477
|
-
};
|
|
1478
|
-
// Handle internal tool execution in task mode
|
|
1479
|
-
if (toolStatus === TOOL_STATUS.SUCCEEDED && tool.type === TOOL_TYPE.INTERNAL) {
|
|
1480
|
-
const progressTracker = createProgressTracker(progressToken, extra.sendNotification, taskId, onStatusMessage);
|
|
1481
|
-
try {
|
|
1482
|
-
log.info('Calling internal tool for task', {
|
|
1483
|
-
taskId,
|
|
1484
|
-
toolName: tool.name,
|
|
1485
|
-
mcpSessionId,
|
|
1486
|
-
input: logSafeArgs,
|
|
1487
|
-
});
|
|
1488
|
-
const res = (await tool.call({
|
|
1489
|
-
args: toolArgs,
|
|
1490
|
-
extra: taskExtra,
|
|
1491
1173
|
apifyMcpServer: this,
|
|
1492
|
-
mcpServer: this.server,
|
|
1493
|
-
apifyToken,
|
|
1494
|
-
apifyClient,
|
|
1495
|
-
progressTracker,
|
|
1496
|
-
mcpSessionId,
|
|
1497
|
-
taskMode: true,
|
|
1498
|
-
}));
|
|
1499
|
-
({ toolStatus, callDiagnostics } = applyToolTelemetry(res, actorName, actorId, callDiagnostics));
|
|
1500
|
-
result = res;
|
|
1501
|
-
}
|
|
1502
|
-
finally {
|
|
1503
|
-
if (progressTracker) {
|
|
1504
|
-
progressTracker.stop();
|
|
1505
|
-
}
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1508
|
-
// Handle actor tool execution in task mode
|
|
1509
|
-
if (toolStatus === TOOL_STATUS.SUCCEEDED && tool.type === TOOL_TYPE.ACTOR) {
|
|
1510
|
-
const progressTracker = createProgressTracker(progressToken, extra.sendNotification, taskId, onStatusMessage);
|
|
1511
|
-
try {
|
|
1512
|
-
log.info('Calling Actor for task', {
|
|
1513
|
-
taskId,
|
|
1514
|
-
toolName: tool.name,
|
|
1515
|
-
actorName: tool.actorFullName,
|
|
1516
|
-
mcpSessionId,
|
|
1517
|
-
input: logSafeArgs,
|
|
1518
1174
|
});
|
|
1519
|
-
const executorResult = await actorExecutor.executeActorTool({
|
|
1520
|
-
actorFullName: tool.actorFullName,
|
|
1521
|
-
input: toolArgs,
|
|
1522
|
-
apifyClient,
|
|
1523
|
-
callOptions: { memory: tool.memoryMbytes },
|
|
1524
|
-
progressTracker,
|
|
1525
|
-
abortSignal: cancelWatcher.signal,
|
|
1526
|
-
mcpSessionId,
|
|
1527
|
-
datasetItemsSchema: tool.datasetItemsSchema,
|
|
1528
|
-
taskMode: true,
|
|
1529
|
-
});
|
|
1530
|
-
if (!executorResult) {
|
|
1531
|
-
toolStatus = TOOL_STATUS.ABORTED;
|
|
1532
|
-
// Receivers of cancellation notifications SHOULD NOT send a response for the cancelled request
|
|
1533
|
-
// https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/cancellation#behavior-requirements
|
|
1534
|
-
result = {};
|
|
1535
|
-
}
|
|
1536
|
-
else {
|
|
1537
|
-
// Mirror the INTERNAL task branch: extract embedded telemetry so failure_category
|
|
1538
|
-
// drives the success-path nudge below and toolTelemetry is stripped from the wire.
|
|
1539
|
-
({ toolStatus, callDiagnostics } = applyToolTelemetry(executorResult, actorName, actorId, callDiagnostics));
|
|
1540
|
-
result = executorResult;
|
|
1541
|
-
}
|
|
1542
|
-
}
|
|
1543
|
-
finally {
|
|
1544
|
-
if (progressTracker) {
|
|
1545
|
-
progressTracker.stop();
|
|
1546
|
-
}
|
|
1547
1175
|
}
|
|
1548
1176
|
}
|
|
1549
|
-
|
|
1550
|
-
if (await skipIfTaskCancelled(', skipping result storage', toolStatus))
|
|
1551
|
-
return;
|
|
1552
|
-
// On a failed result, nudge the agent to report the blocker via report-problem (mirrors the
|
|
1553
|
-
// synchronous CallTool path, which task-mode calls like call-actor bypass).
|
|
1554
|
-
result = this.withReportProblemNudge(result, tool.name, callDiagnostics.failure_category, callDiagnostics.failure_http_status);
|
|
1555
|
-
// Store the result in the task store
|
|
1556
|
-
log.debug('[executeToolAndUpdateTask] Storing completed result', {
|
|
1557
|
-
taskId,
|
|
1558
|
-
mcpSessionId,
|
|
1559
|
-
});
|
|
1560
|
-
await storeTaskResultOrSkipIfExpired(this.taskStore, tool.name, taskId, 'completed', result, mcpSessionId);
|
|
1561
|
-
log.debug('Task completed successfully', { taskId, toolName: tool.name, mcpSessionId });
|
|
1562
|
-
await emitTaskStatusNotification(taskId, mcpSessionId, this.taskStore, this.server);
|
|
1563
|
-
finishTaskTracking(toolStatus, callDiagnostics, result);
|
|
1564
|
-
}
|
|
1565
|
-
catch (error) {
|
|
1566
|
-
// Reached only when the task expired before the `working` transition (updateTaskStatus
|
|
1567
|
-
// above rethrows the store's unknown-taskId error). The tool never ran and the task is
|
|
1568
|
-
// gone, so soft-fail, record telemetry, and stop. Every result store (success and error
|
|
1569
|
-
// paths) tolerates expiry via storeTaskResultOrSkipIfExpired, so they don't reach here.
|
|
1570
|
-
if (isTaskNotFoundError(error)) {
|
|
1571
|
-
log.softFail('Task expired before execution started', {
|
|
1572
|
-
taskId,
|
|
1573
|
-
toolName: tool.name,
|
|
1574
|
-
mcpSessionId,
|
|
1575
|
-
});
|
|
1576
|
-
finishTaskTracking(TOOL_STATUS.SOFT_FAIL, {
|
|
1577
|
-
failure_category: FAILURE_CATEGORY.INTERNAL_ERROR,
|
|
1578
|
-
...buildActorFields(actorName, actorId),
|
|
1579
|
-
});
|
|
1580
|
-
return;
|
|
1581
|
-
}
|
|
1582
|
-
// Handle 402 Payment Required — return structured x402 result so clients can auto-pay
|
|
1583
|
-
const httpStatus = getHttpStatusCode(error);
|
|
1584
|
-
if (isX402PaymentRequiredError(error)) {
|
|
1585
|
-
logHttpError(error, 'Payment required while calling tool (task mode)', { toolName: tool.name });
|
|
1586
|
-
// Per MCP tasks spec: once a task is cancelled it MUST remain cancelled,
|
|
1587
|
-
// so guard storeTaskResult against a cancel that raced with this 402.
|
|
1588
|
-
if (await skipIfTaskCancelled(', skipping 402 result storage', TOOL_STATUS.ABORTED, {
|
|
1589
|
-
...buildActorFields(actorName, actorId),
|
|
1590
|
-
}))
|
|
1591
|
-
return;
|
|
1592
|
-
const paymentResponse = buildPaymentRequiredResponse(error);
|
|
1593
|
-
await storeTaskResultOrSkipIfExpired(this.taskStore, tool.name, taskId, 'completed', paymentResponse, mcpSessionId);
|
|
1594
|
-
await emitTaskStatusNotification(taskId, mcpSessionId, this.taskStore, this.server);
|
|
1595
|
-
finishTaskTracking(TOOL_STATUS.SOFT_FAIL, {
|
|
1596
|
-
failure_category: FAILURE_CATEGORY.INVALID_INPUT,
|
|
1597
|
-
failure_http_status: 402,
|
|
1598
|
-
...buildActorFields(actorName, actorId),
|
|
1599
|
-
}, paymentResponse);
|
|
1600
|
-
return;
|
|
1601
|
-
}
|
|
1602
|
-
if (isPermissionApprovalError(error)) {
|
|
1603
|
-
logHttpError(error, 'Permission approval required while calling tool (task mode)', {
|
|
1604
|
-
toolName: tool.name,
|
|
1605
|
-
});
|
|
1606
|
-
// Per MCP tasks spec: once a task is cancelled it MUST remain cancelled,
|
|
1607
|
-
// so guard storeTaskResult against a cancel that raced with this approval error.
|
|
1608
|
-
if (await skipIfTaskCancelled(', skipping permission-approval result storage', TOOL_STATUS.ABORTED, {
|
|
1609
|
-
...buildActorFields(actorName, actorId),
|
|
1610
|
-
}))
|
|
1611
|
-
return;
|
|
1612
|
-
const approvalResponse = buildPermissionApprovalResponse(error);
|
|
1613
|
-
await storeTaskResultOrSkipIfExpired(this.taskStore, tool.name, taskId, 'completed', approvalResponse, mcpSessionId);
|
|
1614
|
-
await emitTaskStatusNotification(taskId, mcpSessionId, this.taskStore, this.server);
|
|
1615
|
-
finishTaskTracking(TOOL_STATUS.SOFT_FAIL, {
|
|
1616
|
-
failure_category: FAILURE_CATEGORY.PERMISSION_APPROVAL_REQUIRED,
|
|
1617
|
-
failure_http_status: error.statusCode,
|
|
1618
|
-
...buildActorFields(actorName, actorId),
|
|
1619
|
-
}, approvalResponse);
|
|
1620
|
-
return;
|
|
1621
|
-
}
|
|
1622
|
-
toolStatus = getToolStatusFromError(error, Boolean(cancelWatcher.signal.aborted));
|
|
1623
|
-
const failureDetail = error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200);
|
|
1624
|
-
callDiagnostics = {
|
|
1625
|
-
failure_category: classifyFailureCategory(error),
|
|
1626
|
-
...(httpStatus !== undefined ? { failure_http_status: httpStatus } : {}),
|
|
1627
|
-
failure_detail: failureDetail,
|
|
1628
|
-
...buildActorFields(actorName, actorId),
|
|
1629
|
-
};
|
|
1630
|
-
// Log level follows the already-classified toolStatus:
|
|
1631
|
-
// SOFT_FAIL (e.g. 402/403 user quota, client-side issues) → softFail
|
|
1632
|
-
// FAILED/ABORTED/other → error
|
|
1633
|
-
if (toolStatus === TOOL_STATUS.SOFT_FAIL) {
|
|
1634
|
-
// Mezmo promotes on "error" in message/keys — use errMessage key, sanitized.
|
|
1635
|
-
const errMessage = sanitizeMezmoMessage(error instanceof Error ? error.message : String(error));
|
|
1636
|
-
log.softFail('Tool execution soft-failed for task', {
|
|
1637
|
-
taskId,
|
|
1638
|
-
toolName: tool.name,
|
|
1639
|
-
toolStatus,
|
|
1640
|
-
mcpSessionId,
|
|
1641
|
-
failureCategory: callDiagnostics.failure_category,
|
|
1642
|
-
failureHttpStatus: callDiagnostics.failure_http_status,
|
|
1643
|
-
actorName: callDiagnostics.actor_name,
|
|
1644
|
-
errMessage,
|
|
1645
|
-
});
|
|
1646
|
-
}
|
|
1647
|
-
else {
|
|
1648
|
-
log.error('Error executing tool for task', {
|
|
1649
|
-
taskId,
|
|
1650
|
-
toolName: tool.name,
|
|
1651
|
-
toolStatus,
|
|
1652
|
-
mcpSessionId,
|
|
1653
|
-
failureCategory: callDiagnostics.failure_category,
|
|
1654
|
-
failureHttpStatus: callDiagnostics.failure_http_status,
|
|
1655
|
-
actorName: callDiagnostics.actor_name,
|
|
1656
|
-
error,
|
|
1657
|
-
});
|
|
1658
|
-
}
|
|
1659
|
-
const userText = getToolCallErrorUserText(tool.name, error);
|
|
1660
|
-
// Check if task was cancelled before storing result
|
|
1661
|
-
if (await skipIfTaskCancelled(', skipping result storage', toolStatus, callDiagnostics))
|
|
1662
|
-
return;
|
|
1663
|
-
log.debug('[executeToolAndUpdateTask] Storing failed result', {
|
|
1664
|
-
taskId,
|
|
1665
|
-
mcpSessionId,
|
|
1666
|
-
});
|
|
1667
|
-
// Nudge on a genuinely-failed task result. INTERNAL_ERROR and an unknown category get the
|
|
1668
|
-
// full nudge; a genuine INVALID_INPUT gets the softer nudge; payment (402) is suppressed via
|
|
1669
|
-
// the HTTP status and AUTH / PERMISSION_APPROVAL_REQUIRED via NON_NUDGE_FAILURE_CATEGORIES.
|
|
1670
|
-
// The 402 branch above returns before reaching here, so this only sees non-payment failures.
|
|
1671
|
-
const failedResult = this.withReportProblemNudge({
|
|
1672
|
-
content: [
|
|
1673
|
-
{
|
|
1674
|
-
type: 'text',
|
|
1675
|
-
text: userText,
|
|
1676
|
-
},
|
|
1677
|
-
],
|
|
1678
|
-
isError: true,
|
|
1679
|
-
internalToolStatus: toolStatus,
|
|
1680
|
-
}, tool.name, callDiagnostics.failure_category, callDiagnostics.failure_http_status);
|
|
1681
|
-
await storeTaskResultOrSkipIfExpired(this.taskStore, tool.name, taskId, 'failed', failedResult, mcpSessionId);
|
|
1682
|
-
await emitTaskStatusNotification(taskId, mcpSessionId, this.taskStore, this.server);
|
|
1683
|
-
finishTaskTracking(toolStatus, callDiagnostics, failedResult);
|
|
1684
|
-
}
|
|
1685
|
-
finally {
|
|
1686
|
-
cancelWatcher.dispose();
|
|
1687
|
-
}
|
|
1688
|
-
}
|
|
1689
|
-
/*
|
|
1690
|
-
* Creates telemetry data for a tool call.
|
|
1691
|
-
*/
|
|
1692
|
-
async prepareTelemetryData(toolName, mcpSessionId, apifyToken) {
|
|
1693
|
-
var _a, _b, _c, _d, _e;
|
|
1694
|
-
if (!this.telemetryEnabled) {
|
|
1695
|
-
return { telemetryData: null, userId: null };
|
|
1696
|
-
}
|
|
1697
|
-
// Get userId from cache or fetch from API
|
|
1698
|
-
let userId = null;
|
|
1699
|
-
if (apifyToken) {
|
|
1700
|
-
const apifyClient = new ApifyClient({ token: apifyToken });
|
|
1701
|
-
({ userId } = await getUserInfoCached(apifyToken, apifyClient));
|
|
1702
|
-
log.debug('Telemetry: fetched userId', { userId, mcpSessionId });
|
|
1703
|
-
}
|
|
1704
|
-
const capabilities = (_b = (_a = this.options.initializeRequestData) === null || _a === void 0 ? void 0 : _a.params) === null || _b === void 0 ? void 0 : _b.capabilities;
|
|
1705
|
-
const params = (_c = this.options.initializeRequestData) === null || _c === void 0 ? void 0 : _c.params;
|
|
1706
|
-
const telemetryData = {
|
|
1707
|
-
app: 'mcp',
|
|
1708
|
-
app_version: getPackageVersion() || '',
|
|
1709
|
-
mcp_client_name: ((_d = params === null || params === void 0 ? void 0 : params.clientInfo) === null || _d === void 0 ? void 0 : _d.name) || '',
|
|
1710
|
-
mcp_client_version: ((_e = params === null || params === void 0 ? void 0 : params.clientInfo) === null || _e === void 0 ? void 0 : _e.version) || '',
|
|
1711
|
-
mcp_protocol_version: (params === null || params === void 0 ? void 0 : params.protocolVersion) || '',
|
|
1712
|
-
mcp_client_capabilities: capabilities || null,
|
|
1713
|
-
mcp_session_id: mcpSessionId || '',
|
|
1714
|
-
transport_type: this.options.transportType || '',
|
|
1715
|
-
tool_name: toolName,
|
|
1716
|
-
tool_status: TOOL_STATUS.SUCCEEDED, // Will be updated in finally
|
|
1717
|
-
tool_exec_time_ms: 0, // Will be calculated in finally
|
|
1718
|
-
};
|
|
1719
|
-
return { telemetryData, userId };
|
|
1177
|
+
});
|
|
1720
1178
|
}
|
|
1721
1179
|
/**
|
|
1722
1180
|
* Resolves widgets and determines which ones are ready to be served.
|