@apify/actors-mcp-server 0.11.7-beta.5 → 0.11.7-beta.7

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.
@@ -843,7 +843,7 @@ export class ActorsMcpServer {
843
843
  * @throws {McpError} - based on the McpServer class code from the typescript MCP SDK
844
844
  */
845
845
  this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
846
- var _a, _b, _c, _d, _e, _f;
846
+ var _a, _b, _c, _d, _e;
847
847
  const params = request.params;
848
848
  // eslint-disable-next-line prefer-const
849
849
  let { name, arguments: args, _meta: meta } = params;
@@ -1090,152 +1090,33 @@ export class ActorsMcpServer {
1090
1090
  callDiagnostics = outcome.callDiagnostics;
1091
1091
  return captureResult(outcome.result);
1092
1092
  }
1093
- // Handle internal tool
1094
- if (tool.type === TOOL_TYPE.INTERNAL) {
1095
- // Tools that may emit notifications/progress during a sync wait must be opted in here.
1096
- // call-actor: emits during start+waitForFinish. get-actor-run: emits when waitSecs > 0.
1097
- const progressTrackerOptIn = tool.name === HELPER_TOOLS.ACTOR_CALL || tool.name === HELPER_TOOLS.ACTOR_RUNS_GET;
1098
- const progressTracker = progressTrackerOptIn
1099
- ? createProgressTracker(progressToken, extra.sendNotification)
1100
- : null;
1101
- try {
1102
- log.info('Calling internal tool', { toolName: tool.name, mcpSessionId, input: logSafeArgs });
1103
- const res = (await tool.call({
1104
- args: toolArgs,
1105
- extra,
1106
- apifyMcpServer: this,
1107
- mcpServer: this.server,
1108
- apifyToken,
1109
- apifyClient: apifyClient,
1110
- progressTracker,
1111
- mcpSessionId,
1112
- }));
1113
- // Extract diagnostics and strip internal fields from res before returning to client.
1114
- ({ toolStatus, callDiagnostics } = applyToolTelemetry(res, actorName, actorId, callDiagnostics));
1115
- return captureResult(res);
1116
- }
1117
- finally {
1118
- progressTracker === null || progressTracker === void 0 ? void 0 : progressTracker.stop();
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;
1093
+ // Progress tracker: opt in for the two INTERNAL tools that emit during a sync wait
1094
+ // (call-actor start+waitForFinish, get-actor-run when waitSecs > 0), and unconditionally
1095
+ // for ACTOR tools. ACTOR_MCP forwards notifications directly, not via a tracker.
1096
+ const progressTrackerOptIn = tool.type === TOOL_TYPE.ACTOR ||
1097
+ (tool.type === TOOL_TYPE.INTERNAL &&
1098
+ (tool.name === HELPER_TOOLS.ACTOR_CALL || tool.name === HELPER_TOOLS.ACTOR_RUNS_GET));
1099
+ const progressTracker = progressTrackerOptIn
1100
+ ? createProgressTracker(progressToken, extra.sendNotification)
1101
+ : null;
1102
+ const dispatchResult = await this.dispatchToolCall({
1103
+ tool,
1104
+ toolArgs: toolArgs,
1105
+ logSafeArgs,
1106
+ apifyToken,
1107
+ apifyClient: apifyClient,
1108
+ mcpSessionId,
1109
+ progressToken,
1110
+ progressTracker,
1111
+ shouldForwardNotifications: true,
1112
+ extra,
1113
+ actorName,
1114
+ actorId,
1115
+ taskMode: false,
1116
+ });
1117
+ toolStatus = dispatchResult.toolStatus;
1118
+ callDiagnostics = dispatchResult.callDiagnostics;
1119
+ return captureResult(dispatchResult.result);
1239
1120
  }
1240
1121
  catch (error) {
1241
1122
  // Re-throw MCP protocol errors (e.g. from failInvalidParams) so the SDK
@@ -1244,8 +1125,8 @@ export class ActorsMcpServer {
1244
1125
  // Re-throwing first is order-equivalent only because no McpError with an HTTP-range
1245
1126
  // code reaches this catch: an McpError(402) WOULD satisfy the x402 predicate
1246
1127
  // (getHttpStatusCode falls through to `.code`), but every remote-McpError route is
1247
- // sealed by its own inner catch before reaching here, and all in-repo McpErrors use
1248
- // negative ErrorCode.* values.
1128
+ // sealed by an inner catch (ACTOR_MCP branch, call-actor MCP passthrough) and all
1129
+ // in-repo McpErrors use negative ErrorCode.* values.
1249
1130
  if (error instanceof McpError) {
1250
1131
  throw error;
1251
1132
  }
@@ -1253,7 +1134,7 @@ export class ActorsMcpServer {
1253
1134
  toolName: name,
1254
1135
  actorName,
1255
1136
  actorId,
1256
- isAborted: Boolean((_f = extra.signal) === null || _f === void 0 ? void 0 : _f.aborted),
1137
+ isAborted: Boolean((_e = extra.signal) === null || _e === void 0 ? void 0 : _e.aborted),
1257
1138
  });
1258
1139
  toolStatus = errorResult.toolStatus;
1259
1140
  // Propagate 402 Payment Required as a tool result per x402 MCP transport spec:
@@ -1316,18 +1197,6 @@ export class ActorsMcpServer {
1316
1197
  });
1317
1198
  }
1318
1199
  }
1319
- const availableTools = this.listToolNames();
1320
- const msg = dedent `
1321
- Unknown tool type for "${name}".
1322
- Available tools: ${availableTools.length > 0 ? availableTools.join(', ') : 'none'}.
1323
- Please verify the tool name and ensure the tool is properly registered.
1324
- `;
1325
- log.softFail(msg, { mcpSessionId, statusCode: 404 });
1326
- await this.server.sendLoggingMessage({
1327
- level: 'error',
1328
- data: msg,
1329
- });
1330
- throw new McpError(ErrorCode.InvalidParams, msg);
1331
1200
  });
1332
1201
  }
1333
1202
  /**
@@ -1379,7 +1248,201 @@ export class ActorsMcpServer {
1379
1248
  }
1380
1249
  }
1381
1250
  }
1382
- // TODO: this function quite duplicates the main tool call login the CallToolRequestSchema handler, we should refactor
1251
+ /**
1252
+ * Runs a validated tool call through the single tool-type dispatch switch and returns the raw
1253
+ * result plus derived telemetry. The exhaustive switch turns a future 4th TOOL_TYPE into a compile
1254
+ * error (same `satisfies never` idiom as getToolFullName) instead of a silent runtime fall-through.
1255
+ * Both the sync `CallToolRequestSchema` handler and `executeToolAndUpdateTask` call this after their
1256
+ * own pre-dispatch validation and keep their own try/catch + buildToolCallErrorResult around it.
1257
+ * INTERNAL and ACTOR execution errors propagate to the caller's catch; the ACTOR_MCP case keeps its
1258
+ * own inner catch and returns a soft-fail result instead of throwing.
1259
+ *
1260
+ * `extractToolTelemetry` (via `applyToolTelemetry`) runs once here, in the INTERNAL and ACTOR
1261
+ * cases, and strips `toolTelemetry` in place, so callers must not re-strip. ACTOR_MCP sets
1262
+ * telemetry manually instead. The caller constructs `progressTracker`; dispatch consumes it and stops
1263
+ * it in the branch `finally`. The abort source is `extra.signal`: the request signal for sync; the
1264
+ * task caller passes a `taskExtra` whose `signal` is the cancel watcher's, so a client disconnect
1265
+ * never cancels a task. `shouldForwardNotifications` gates only the ACTOR_MCP raw-notification
1266
+ * forwarder (true for sync, false for task, whose originating request is already answered).
1267
+ * `taskMode` is a passthrough to `tool.call` / `executeActorTool`, never a dispatch selector.
1268
+ */
1269
+ async dispatchToolCall(params) {
1270
+ var _a, _b, _c, _d;
1271
+ const { tool, toolArgs, logSafeArgs, apifyToken, apifyClient, mcpSessionId, progressToken, progressTracker, shouldForwardNotifications, extra, actorName, actorId, taskMode, logContext, } = params;
1272
+ let result = {};
1273
+ let toolStatus = TOOL_STATUS.SUCCEEDED;
1274
+ // Always populate actor fields so they're tracked on both success and failure paths.
1275
+ let callDiagnostics = { ...buildActorFields(actorName, actorId) };
1276
+ switch (tool.type) {
1277
+ case TOOL_TYPE.INTERNAL: {
1278
+ try {
1279
+ log.info(`Calling internal tool${(_a = logContext === null || logContext === void 0 ? void 0 : logContext.messageSuffix) !== null && _a !== void 0 ? _a : ''}`, {
1280
+ ...logContext === null || logContext === void 0 ? void 0 : logContext.fields,
1281
+ toolName: tool.name,
1282
+ mcpSessionId,
1283
+ input: logSafeArgs,
1284
+ });
1285
+ const res = (await tool.call({
1286
+ args: toolArgs,
1287
+ extra,
1288
+ apifyMcpServer: this,
1289
+ mcpServer: this.server,
1290
+ apifyToken,
1291
+ apifyClient,
1292
+ progressTracker,
1293
+ mcpSessionId,
1294
+ taskMode,
1295
+ }));
1296
+ // Extract diagnostics and strip internal fields from res before returning to client.
1297
+ ({ toolStatus, callDiagnostics } = applyToolTelemetry(res, actorName, actorId, callDiagnostics));
1298
+ result = res;
1299
+ }
1300
+ finally {
1301
+ progressTracker === null || progressTracker === void 0 ? void 0 : progressTracker.stop();
1302
+ }
1303
+ break;
1304
+ }
1305
+ case TOOL_TYPE.ACTOR_MCP: {
1306
+ // This case never throws: connect/exec failures resolve to a soft-fail `result`
1307
+ // (isError body) below instead. As a task, that means the outer completeTask
1308
+ // stores it via the 'completed' path (isError body) — deliberately matching sync's
1309
+ // own soft-fail semantics, unlike ACTOR/INTERNAL, whose thrown errors land in the
1310
+ // task caller's 'failed' path.
1311
+ let client = null;
1312
+ try {
1313
+ client = await connectMCPClient(tool.serverUrl, apifyToken, mcpSessionId);
1314
+ if (!client) {
1315
+ const msg = dedent `
1316
+ Failed to connect to MCP server at "${tool.serverUrl}".
1317
+ Please verify the server URL is correct and accessible, and ensure you have a valid Apify token with appropriate permissions.
1318
+ `;
1319
+ log.softFail(msg, { mcpSessionId, failureCategory: FAILURE_CATEGORY.INTERNAL_ERROR });
1320
+ await this.server.sendLoggingMessage({ level: 'error', data: msg });
1321
+ toolStatus = TOOL_STATUS.SOFT_FAIL;
1322
+ callDiagnostics = { ...callDiagnostics, failure_category: FAILURE_CATEGORY.INTERNAL_ERROR };
1323
+ result = respondErrorNoTelemetry(msg);
1324
+ break;
1325
+ }
1326
+ // Only set up notification handlers if progressToken is provided by the client.
1327
+ // Gated off for tasks (shouldForwardNotifications=false): the originating request is
1328
+ // already answered, so forwarding against its progressToken would misroute.
1329
+ if (shouldForwardNotifications && progressToken !== undefined && progressToken !== null) {
1330
+ // Set up notification handlers for the client
1331
+ for (const schema of ServerNotificationSchema.options) {
1332
+ const method = schema.shape.method.value;
1333
+ // Forward notifications from the proxy client to the server
1334
+ client.setNotificationHandler(schema, async (notification) => {
1335
+ log.debug('Sending MCP notification', {
1336
+ method,
1337
+ mcpSessionId,
1338
+ notification,
1339
+ });
1340
+ await extra.sendNotification(notification);
1341
+ });
1342
+ }
1343
+ }
1344
+ log.info(`Calling Actor-MCP${(_b = logContext === null || logContext === void 0 ? void 0 : logContext.messageSuffix) !== null && _b !== void 0 ? _b : ''}`, {
1345
+ ...logContext === null || logContext === void 0 ? void 0 : logContext.fields,
1346
+ toolName: tool.name,
1347
+ actorMcpToolName: tool.originToolName,
1348
+ actorId: tool.actorId,
1349
+ mcpSessionId,
1350
+ input: logSafeArgs,
1351
+ });
1352
+ const res = await client.callTool({
1353
+ name: tool.originToolName,
1354
+ arguments: toolArgs,
1355
+ // Without forwarding there is no route back for remote progress — don't
1356
+ // hand the remote a token nobody listens to.
1357
+ ...(shouldForwardNotifications ? { _meta: { progressToken } } : {}),
1358
+ }, CallToolResultSchema, {
1359
+ timeout: EXTERNAL_TOOL_CALL_TIMEOUT_MSEC,
1360
+ });
1361
+ // TODO: actor-mcp responses are opaque — isError could be a user input problem
1362
+ // (e.g. invalid query) or a genuine server failure. We can't distinguish without
1363
+ // parsing the error text. Defaulting to INTERNAL_ERROR for now; revisit when
1364
+ // actor-mcp gets deeper telemetry treatment.
1365
+ if ('isError' in res && res.isError) {
1366
+ toolStatus = TOOL_STATUS.SOFT_FAIL;
1367
+ callDiagnostics = {
1368
+ failure_category: FAILURE_CATEGORY.INTERNAL_ERROR,
1369
+ ...buildActorFields(actorName, actorId),
1370
+ };
1371
+ }
1372
+ result = { ...res };
1373
+ }
1374
+ catch (error) {
1375
+ toolStatus = getToolStatusFromError(error, Boolean((_c = extra.signal) === null || _c === void 0 ? void 0 : _c.aborted));
1376
+ const failureDetail = error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200);
1377
+ callDiagnostics = {
1378
+ failure_category: classifyFailureCategory(error),
1379
+ failure_detail: failureDetail,
1380
+ ...buildActorFields(actorName, actorId),
1381
+ };
1382
+ logHttpError(error, `Failed to call MCP tool '${tool.originToolName}' on Actor '${tool.actorId}'`, {
1383
+ actorId: tool.actorId,
1384
+ toolName: tool.originToolName,
1385
+ failureCategory: callDiagnostics.failure_category,
1386
+ });
1387
+ result = respondErrorNoTelemetry(`Failed to call MCP tool '${tool.originToolName}' on Actor '${tool.actorId}': ${remoteMcpFailureDetail(error)}`);
1388
+ }
1389
+ finally {
1390
+ if (client)
1391
+ await client.close();
1392
+ }
1393
+ break;
1394
+ }
1395
+ case TOOL_TYPE.ACTOR: {
1396
+ try {
1397
+ log.info(`Calling Actor${(_d = logContext === null || logContext === void 0 ? void 0 : logContext.messageSuffix) !== null && _d !== void 0 ? _d : ''}`, {
1398
+ ...logContext === null || logContext === void 0 ? void 0 : logContext.fields,
1399
+ toolName: tool.name,
1400
+ actorName: tool.actorFullName,
1401
+ mcpSessionId,
1402
+ input: logSafeArgs,
1403
+ });
1404
+ const executorResult = await actorExecutor.executeActorTool({
1405
+ actorFullName: tool.actorFullName,
1406
+ input: toolArgs,
1407
+ apifyClient,
1408
+ callOptions: { memory: tool.memoryMbytes },
1409
+ progressTracker,
1410
+ abortSignal: extra.signal,
1411
+ mcpSessionId,
1412
+ datasetItemsSchema: tool.datasetItemsSchema,
1413
+ taskMode,
1414
+ });
1415
+ if (!executorResult) {
1416
+ toolStatus = TOOL_STATUS.ABORTED;
1417
+ // Receivers of cancellation notifications SHOULD NOT send a response for the cancelled request
1418
+ // https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/cancellation#behavior-requirements
1419
+ result = {};
1420
+ break;
1421
+ }
1422
+ // Mirror the INTERNAL branch: read the telemetry the executor embedded on error
1423
+ // results (e.g. respondUserError → SOFT_FAIL/INVALID_INPUT), strip it from the wire,
1424
+ // and set failure_category so the report-problem nudge picks the softer variant.
1425
+ ({ toolStatus, callDiagnostics } = applyToolTelemetry(executorResult, actorName, actorId, callDiagnostics));
1426
+ result = executorResult;
1427
+ }
1428
+ finally {
1429
+ if (progressTracker) {
1430
+ progressTracker.stop();
1431
+ }
1432
+ }
1433
+ break;
1434
+ }
1435
+ default:
1436
+ // Exhaustiveness guard mirroring getToolFullName: a new TOOL_TYPE member makes `tool`
1437
+ // non-`never` here and fails `satisfies never` at compile time. Unreachable at runtime —
1438
+ // ToolEntry is a closed 3-way union. Unlike the pre-extraction fall-through (which also
1439
+ // listed available tools, called log.softFail, and sent a logging message before
1440
+ // throwing), this just throws InvalidParams with no side effects.
1441
+ throw new McpError(ErrorCode.InvalidParams, `Unknown tool type "${tool.type}"`);
1442
+ }
1443
+ return { result, toolStatus, callDiagnostics };
1444
+ }
1445
+ // TODO: outer orchestration here (pre-flight, telemetry bookkeeping, task-store handling) still duplicates the CallToolRequestSchema handler's logic; the dispatch ladder is now shared. Refactor.
1383
1446
  /**
1384
1447
  * Executes a tool asynchronously for a long-running task and updates task status.
1385
1448
  *
@@ -1420,6 +1483,13 @@ export class ActorsMcpServer {
1420
1483
  result,
1421
1484
  });
1422
1485
  };
1486
+ // Terminal step shared by every task outcome: store the result (expiry-tolerant),
1487
+ // emit the status notification, record telemetry.
1488
+ const completeTask = async (storeStatus, taskResult, status, diagnostics) => {
1489
+ await storeTaskResultOrSkipIfExpired(this.taskStore, tool.name, taskId, storeStatus, taskResult, mcpSessionId);
1490
+ await emitTaskStatusNotification(taskId, mcpSessionId, this.taskStore, this.server);
1491
+ finishTaskTracking(status, diagnostics, taskResult);
1492
+ };
1423
1493
  // Once a task is cancelled the spec forbids writing a result; every storage path
1424
1494
  // must short-circuit here. `logSuffix` is concatenated after "Task was cancelled"
1425
1495
  // so we keep the existing log format and the existing telemetry status per path.
@@ -1472,77 +1542,31 @@ export class ActorsMcpServer {
1472
1542
  await this.taskStore.updateTaskStatus(taskId, 'working', message, mcpSessionId);
1473
1543
  await emitTaskStatusNotification(taskId, mcpSessionId, this.taskStore, this.server);
1474
1544
  };
1475
- // Handle internal tool execution in task mode
1476
- if (toolStatus === TOOL_STATUS.SUCCEEDED && tool.type === TOOL_TYPE.INTERNAL) {
1477
- const progressTracker = createProgressTracker(progressToken, extra.sendNotification, taskId, onStatusMessage);
1478
- try {
1479
- log.info('Calling internal tool for task', {
1480
- taskId,
1481
- toolName: tool.name,
1482
- mcpSessionId,
1483
- input: logSafeArgs,
1484
- });
1485
- const res = (await tool.call({
1486
- args: toolArgs,
1487
- extra: taskExtra,
1488
- apifyMcpServer: this,
1489
- mcpServer: this.server,
1490
- apifyToken,
1491
- apifyClient,
1492
- progressTracker,
1493
- mcpSessionId,
1494
- taskMode: true,
1495
- }));
1496
- ({ toolStatus, callDiagnostics } = applyToolTelemetry(res, actorName, actorId, callDiagnostics));
1497
- result = res;
1498
- }
1499
- finally {
1500
- if (progressTracker) {
1501
- progressTracker.stop();
1502
- }
1503
- }
1504
- }
1505
- // Handle actor tool execution in task mode
1506
- if (toolStatus === TOOL_STATUS.SUCCEEDED && tool.type === TOOL_TYPE.ACTOR) {
1507
- const progressTracker = createProgressTracker(progressToken, extra.sendNotification, taskId, onStatusMessage);
1508
- try {
1509
- log.info('Calling Actor for task', {
1510
- taskId,
1511
- toolName: tool.name,
1512
- actorName: tool.actorFullName,
1513
- mcpSessionId,
1514
- input: logSafeArgs,
1515
- });
1516
- const executorResult = await actorExecutor.executeActorTool({
1517
- actorFullName: tool.actorFullName,
1518
- input: toolArgs,
1519
- apifyClient,
1520
- callOptions: { memory: tool.memoryMbytes },
1521
- progressTracker,
1522
- abortSignal: cancelWatcher.signal,
1523
- mcpSessionId,
1524
- datasetItemsSchema: tool.datasetItemsSchema,
1525
- taskMode: true,
1526
- });
1527
- if (!executorResult) {
1528
- toolStatus = TOOL_STATUS.ABORTED;
1529
- // Receivers of cancellation notifications SHOULD NOT send a response for the cancelled request
1530
- // https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/cancellation#behavior-requirements
1531
- result = {};
1532
- }
1533
- else {
1534
- // Mirror the INTERNAL task branch: extract embedded telemetry so failure_category
1535
- // drives the success-path nudge below and toolTelemetry is stripped from the wire.
1536
- ({ toolStatus, callDiagnostics } = applyToolTelemetry(executorResult, actorName, actorId, callDiagnostics));
1537
- result = executorResult;
1538
- }
1539
- }
1540
- finally {
1541
- if (progressTracker) {
1542
- progressTracker.stop();
1543
- }
1544
- }
1545
- }
1545
+ // ACTOR_MCP never reads the tracker (matching the sync path, which passes null for it);
1546
+ // INTERNAL/ACTOR get one built from taskId + onStatusMessage, which dispatch consumes
1547
+ // and stops.
1548
+ const progressTracker = tool.type === TOOL_TYPE.ACTOR_MCP
1549
+ ? null
1550
+ : createProgressTracker(progressToken, extra.sendNotification, taskId, onStatusMessage);
1551
+ const dispatchResult = await this.dispatchToolCall({
1552
+ tool,
1553
+ toolArgs,
1554
+ logSafeArgs,
1555
+ apifyToken,
1556
+ apifyClient,
1557
+ mcpSessionId,
1558
+ progressToken,
1559
+ progressTracker,
1560
+ shouldForwardNotifications: false,
1561
+ extra: taskExtra,
1562
+ actorName,
1563
+ actorId,
1564
+ taskMode: true,
1565
+ logContext: { messageSuffix: ' for task', fields: { taskId } },
1566
+ });
1567
+ result = dispatchResult.result;
1568
+ toolStatus = dispatchResult.toolStatus;
1569
+ callDiagnostics = dispatchResult.callDiagnostics;
1546
1570
  // Check if task was cancelled before storing result
1547
1571
  if (await skipIfTaskCancelled(', skipping result storage', toolStatus))
1548
1572
  return;
@@ -1554,10 +1578,8 @@ export class ActorsMcpServer {
1554
1578
  taskId,
1555
1579
  mcpSessionId,
1556
1580
  });
1557
- await storeTaskResultOrSkipIfExpired(this.taskStore, tool.name, taskId, 'completed', result, mcpSessionId);
1581
+ await completeTask('completed', result, toolStatus, callDiagnostics);
1558
1582
  log.debug('Task completed successfully', { taskId, toolName: tool.name, mcpSessionId });
1559
- await emitTaskStatusNotification(taskId, mcpSessionId, this.taskStore, this.server);
1560
- finishTaskTracking(toolStatus, callDiagnostics, result);
1561
1583
  }
1562
1584
  catch (error) {
1563
1585
  // Reached only when the task expired before the `working` transition (updateTaskStatus
@@ -1591,9 +1613,7 @@ export class ActorsMcpServer {
1591
1613
  ...buildActorFields(actorName, actorId),
1592
1614
  }))
1593
1615
  return;
1594
- await storeTaskResultOrSkipIfExpired(this.taskStore, tool.name, taskId, 'completed', errorResult.response, mcpSessionId);
1595
- await emitTaskStatusNotification(taskId, mcpSessionId, this.taskStore, this.server);
1596
- finishTaskTracking(errorResult.toolStatus, errorResult.callDiagnostics, errorResult.response);
1616
+ await completeTask('completed', errorResult.response, errorResult.toolStatus, errorResult.callDiagnostics);
1597
1617
  return;
1598
1618
  }
1599
1619
  if (errorResult.kind === TOOL_CALL_ERROR_KIND.APPROVAL) {
@@ -1606,9 +1626,7 @@ export class ActorsMcpServer {
1606
1626
  ...buildActorFields(actorName, actorId),
1607
1627
  }))
1608
1628
  return;
1609
- await storeTaskResultOrSkipIfExpired(this.taskStore, tool.name, taskId, 'completed', errorResult.response, mcpSessionId);
1610
- await emitTaskStatusNotification(taskId, mcpSessionId, this.taskStore, this.server);
1611
- finishTaskTracking(errorResult.toolStatus, errorResult.callDiagnostics, errorResult.response);
1629
+ await completeTask('completed', errorResult.response, errorResult.toolStatus, errorResult.callDiagnostics);
1612
1630
  return;
1613
1631
  }
1614
1632
  toolStatus = errorResult.toolStatus;
@@ -1650,10 +1668,11 @@ export class ActorsMcpServer {
1650
1668
  taskId,
1651
1669
  mcpSessionId,
1652
1670
  });
1653
- // Nudge on a genuinely-failed task result. INTERNAL_ERROR and an unknown category get the
1654
- // full nudge; a genuine INVALID_INPUT gets the softer nudge; payment (402) is suppressed via
1655
- // the HTTP status and AUTH / PERMISSION_APPROVAL_REQUIRED via NON_NUDGE_FAILURE_CATEGORIES.
1656
- // The 402 branch above returns before reaching here, so this only sees non-payment failures.
1671
+ // Nudge on a genuinely-failed task result (mirrors the completed path and the sync
1672
+ // captureResult). INTERNAL_ERROR and an unknown category get the full nudge; a genuine
1673
+ // INVALID_INPUT gets the softer nudge; payment (402) is suppressed via the HTTP status and
1674
+ // AUTH / PERMISSION_APPROVAL_REQUIRED via NON_NUDGE_FAILURE_CATEGORIES. The 402 branch above
1675
+ // returns before reaching here, so this only sees non-payment failures.
1657
1676
  const failedResult = this.withReportProblemNudge({
1658
1677
  content: [
1659
1678
  {
@@ -1664,9 +1683,7 @@ export class ActorsMcpServer {
1664
1683
  isError: true,
1665
1684
  internalToolStatus: toolStatus,
1666
1685
  }, tool.name, callDiagnostics.failure_category, callDiagnostics.failure_http_status);
1667
- await storeTaskResultOrSkipIfExpired(this.taskStore, tool.name, taskId, 'failed', failedResult, mcpSessionId);
1668
- await emitTaskStatusNotification(taskId, mcpSessionId, this.taskStore, this.server);
1669
- finishTaskTracking(toolStatus, callDiagnostics, failedResult);
1686
+ await completeTask('failed', failedResult, toolStatus, callDiagnostics);
1670
1687
  }
1671
1688
  finally {
1672
1689
  cancelWatcher.dispose();