@apify/actors-mcp-server 0.11.7-beta.0 → 0.11.7-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/dist/const.d.ts +5 -5
  2. package/dist/const.d.ts.map +1 -1
  3. package/dist/const.js +5 -5
  4. package/dist/const.js.map +1 -1
  5. package/dist/index_internals.d.ts +7 -12
  6. package/dist/index_internals.d.ts.map +1 -1
  7. package/dist/index_internals.js +6 -11
  8. package/dist/index_internals.js.map +1 -1
  9. package/dist/mcp/actors.d.ts +0 -4
  10. package/dist/mcp/actors.d.ts.map +1 -1
  11. package/dist/mcp/actors.js +0 -13
  12. package/dist/mcp/actors.js.map +1 -1
  13. package/dist/mcp/server.d.ts +16 -17
  14. package/dist/mcp/server.d.ts.map +1 -1
  15. package/dist/mcp/server.js +204 -442
  16. package/dist/mcp/server.js.map +1 -1
  17. package/dist/mcp/tool_call_error_mapper.d.ts +45 -0
  18. package/dist/mcp/tool_call_error_mapper.d.ts.map +1 -0
  19. package/dist/mcp/tool_call_error_mapper.js +59 -0
  20. package/dist/mcp/tool_call_error_mapper.js.map +1 -0
  21. package/dist/mcp/tool_call_telemetry.d.ts +37 -0
  22. package/dist/mcp/tool_call_telemetry.d.ts.map +1 -0
  23. package/dist/mcp/tool_call_telemetry.js +85 -0
  24. package/dist/mcp/tool_call_telemetry.js.map +1 -0
  25. package/dist/mcp/tool_dispatch.d.ts +49 -0
  26. package/dist/mcp/tool_dispatch.d.ts.map +1 -0
  27. package/dist/mcp/tool_dispatch.js +208 -0
  28. package/dist/mcp/tool_dispatch.js.map +1 -0
  29. package/dist/payments/const.d.ts +1 -0
  30. package/dist/payments/const.d.ts.map +1 -1
  31. package/dist/payments/const.js +1 -0
  32. package/dist/payments/const.js.map +1 -1
  33. package/dist/payments/skyfire.d.ts.map +1 -1
  34. package/dist/payments/skyfire.js +5 -6
  35. package/dist/payments/skyfire.js.map +1 -1
  36. package/dist/resources/api_resources.d.ts +37 -0
  37. package/dist/resources/api_resources.d.ts.map +1 -0
  38. package/dist/resources/api_resources.js +255 -0
  39. package/dist/resources/api_resources.js.map +1 -0
  40. package/dist/resources/resource_service.d.ts +4 -3
  41. package/dist/resources/resource_service.d.ts.map +1 -1
  42. package/dist/resources/resource_service.js +52 -7
  43. package/dist/resources/resource_service.js.map +1 -1
  44. package/dist/tools/actors/call_actor.d.ts +0 -3
  45. package/dist/tools/actors/call_actor.d.ts.map +1 -1
  46. package/dist/tools/actors/call_actor.js +2 -11
  47. package/dist/tools/actors/call_actor.js.map +1 -1
  48. package/dist/tools/actors/fetch_actor_details.d.ts +1 -7
  49. package/dist/tools/actors/fetch_actor_details.d.ts.map +1 -1
  50. package/dist/tools/actors/fetch_actor_details.js +14 -22
  51. package/dist/tools/actors/fetch_actor_details.js.map +1 -1
  52. package/dist/tools/actors/search_actors.d.ts +1 -6
  53. package/dist/tools/actors/search_actors.d.ts.map +1 -1
  54. package/dist/tools/actors/search_actors.js +17 -24
  55. package/dist/tools/actors/search_actors.js.map +1 -1
  56. package/dist/tools/dev/report_problem.d.ts +13 -0
  57. package/dist/tools/dev/report_problem.d.ts.map +1 -1
  58. package/dist/tools/dev/report_problem.js +14 -0
  59. package/dist/tools/dev/report_problem.js.map +1 -1
  60. package/dist/tools/docs/search_apify_docs.d.ts.map +1 -1
  61. package/dist/tools/docs/search_apify_docs.js +1 -0
  62. package/dist/tools/docs/search_apify_docs.js.map +1 -1
  63. package/dist/tools/runs/get_actor_run.d.ts +1 -6
  64. package/dist/tools/runs/get_actor_run.d.ts.map +1 -1
  65. package/dist/tools/runs/get_actor_run.js +17 -24
  66. package/dist/tools/runs/get_actor_run.js.map +1 -1
  67. package/dist/tools/storage/get_dataset.d.ts.map +1 -1
  68. package/dist/tools/storage/get_dataset.js +6 -5
  69. package/dist/tools/storage/get_dataset.js.map +1 -1
  70. package/dist/tools/storage/get_dataset_items.d.ts.map +1 -1
  71. package/dist/tools/storage/get_dataset_items.js +6 -8
  72. package/dist/tools/storage/get_dataset_items.js.map +1 -1
  73. package/dist/tools/storage/get_dataset_list.d.ts.map +1 -1
  74. package/dist/tools/storage/get_dataset_list.js +8 -9
  75. package/dist/tools/storage/get_dataset_list.js.map +1 -1
  76. package/dist/tools/storage/get_dataset_schema.d.ts.map +1 -1
  77. package/dist/tools/storage/get_dataset_schema.js +6 -4
  78. package/dist/tools/storage/get_dataset_schema.js.map +1 -1
  79. package/dist/tools/storage/get_key_value_store.js +3 -3
  80. package/dist/tools/storage/get_key_value_store.js.map +1 -1
  81. package/dist/tools/storage/get_key_value_store_keys.js +4 -4
  82. package/dist/tools/storage/get_key_value_store_keys.js.map +1 -1
  83. package/dist/tools/storage/get_key_value_store_list.d.ts.map +1 -1
  84. package/dist/tools/storage/get_key_value_store_list.js +8 -9
  85. package/dist/tools/storage/get_key_value_store_list.js.map +1 -1
  86. package/dist/tools/storage/get_key_value_store_record.d.ts.map +1 -1
  87. package/dist/tools/storage/get_key_value_store_record.js +12 -9
  88. package/dist/tools/storage/get_key_value_store_record.js.map +1 -1
  89. package/dist/tools/storage/storage_helpers.d.ts +27 -0
  90. package/dist/tools/storage/storage_helpers.d.ts.map +1 -1
  91. package/dist/tools/storage/storage_helpers.js +21 -1
  92. package/dist/tools/storage/storage_helpers.js.map +1 -1
  93. package/dist/tsconfig.tsbuildinfo +1 -1
  94. package/dist/types.d.ts +0 -11
  95. package/dist/types.d.ts.map +1 -1
  96. package/dist/types.js.map +1 -1
  97. package/dist/utils/actor_details.d.ts.map +1 -1
  98. package/dist/utils/actor_details.js +10 -3
  99. package/dist/utils/actor_details.js.map +1 -1
  100. package/dist/utils/logging.d.ts +2 -0
  101. package/dist/utils/logging.d.ts.map +1 -1
  102. package/dist/utils/logging.js +45 -1
  103. package/dist/utils/logging.js.map +1 -1
  104. package/dist/utils/mcp.d.ts +6 -0
  105. package/dist/utils/mcp.d.ts.map +1 -1
  106. package/dist/utils/mcp.js +15 -8
  107. package/dist/utils/mcp.js.map +1 -1
  108. package/dist/utils/payment_errors.d.ts +7 -1
  109. package/dist/utils/payment_errors.d.ts.map +1 -1
  110. package/dist/utils/payment_errors.js +10 -0
  111. package/dist/utils/payment_errors.js.map +1 -1
  112. package/dist/utils/server-instructions/index.d.ts.map +1 -1
  113. package/dist/utils/server-instructions/index.js +10 -0
  114. package/dist/utils/server-instructions/index.js.map +1 -1
  115. package/dist/web/dist/actor-run-widget.js +0 -3
  116. package/dist/web/dist/search-actors-widget.js +0 -3
  117. package/package.json +2 -2
  118. package/dist/web/dist/actor-detail-widget.js +0 -4551
@@ -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, CallToolResultSchema, CancelTaskRequestSchema, ErrorCode, GetPromptRequestSchema, GetTaskPayloadRequestSchema, GetTaskRequestSchema, InitializeRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListTasksRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema, RELATED_TASK_META_KEY, ServerNotificationSchema, SetLevelRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
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,29 +19,26 @@ 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 { buildReportedProblemProperties, getTelemetryEnv, trackReportedProblem, trackToolCall } from '../telemetry.js';
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 { actorExecutor } from '../tools/actors/actor_executor.js';
26
- import { buildPermissionApprovalResponse, checkPaymentProviderStandbyConflict } from '../tools/actors/call_actor.js';
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 { isPermissionApprovalError, remoteMcpFailureDetail } from '../utils/apify_errors.js';
31
- import { getHttpStatusCode, isMcpClientFaultMessage, logHttpError, sanitizeMezmoMessage } from '../utils/logging.js';
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 { applyToolTelemetry, classifyFailureCategory, deriveResourceIds, extractAjvErrorDetails, getToolStatusFromError, } from '../utils/tool_status.js';
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 { getUserInfoCached } from '../utils/userid_cache.js';
42
- import { getPackageVersion } from '../utils/version.js';
43
- import { connectMCPClient } from './client.js';
44
- import { EXTERNAL_TOOL_CALL_TIMEOUT_MSEC, LOG_LEVEL_MAP } from './const.js';
38
+ import { LOG_LEVEL_MAP } from './const.js';
39
+ import { buildToolCallErrorResult, TOOL_CALL_ERROR_KIND } from './tool_call_error_mapper.js';
40
+ import { logToolCallAndTelemetry, prepareTelemetryData } from './tool_call_telemetry.js';
41
+ import { dispatchToolCall } from './tool_dispatch.js';
45
42
  import { createTaskCancellationWatcher, isTaskCancelled, isTaskNotFoundError, parseInputParamsFromUrl, storeTaskResultOrSkipIfExpired, } from './utils.js';
46
43
  /**
47
44
  * Returns true when the initialize request advertises the MCP Apps UI extension
@@ -199,18 +196,18 @@ export class ActorsMcpServer {
199
196
  writable: true,
200
197
  value: []
201
198
  });
202
- // Telemetry configuration (resolved from options and env vars in setupTelemetry)
199
+ // Telemetry configuration (resolved from options and env vars, see setupTelemetry)
203
200
  Object.defineProperty(this, "telemetryEnabled", {
204
201
  enumerable: true,
205
202
  configurable: true,
206
203
  writable: true,
207
- value: null
204
+ value: void 0
208
205
  });
209
206
  Object.defineProperty(this, "telemetryEnv", {
210
207
  enumerable: true,
211
208
  configurable: true,
212
209
  writable: true,
213
- value: DEFAULT_TELEMETRY_ENV
210
+ value: void 0
214
211
  });
215
212
  // List of widgets that are ready to be served
216
213
  Object.defineProperty(this, "availableWidgets", {
@@ -277,7 +274,9 @@ export class ActorsMcpServer {
277
274
  },
278
275
  instructions: getServerInstructions(),
279
276
  });
280
- this.setupTelemetry();
277
+ const { telemetryEnabled, telemetryEnv } = this.setupTelemetry();
278
+ this.telemetryEnabled = telemetryEnabled;
279
+ this.telemetryEnv = telemetryEnv;
281
280
  this.setupInitializeHandler();
282
281
  this.setupLoggingProxy();
283
282
  this.tools = new Map();
@@ -296,18 +295,21 @@ export class ActorsMcpServer {
296
295
  */
297
296
  setupTelemetry() {
298
297
  var _a, _b, _c;
298
+ let telemetryEnabled;
299
299
  const explicitEnabled = parseBooleanOrNull((_a = this.options.telemetry) === null || _a === void 0 ? void 0 : _a.enabled);
300
300
  if (explicitEnabled !== null) {
301
- this.telemetryEnabled = explicitEnabled;
301
+ telemetryEnabled = explicitEnabled;
302
302
  }
303
303
  else {
304
304
  const envEnabled = parseBooleanOrNull(process.env.TELEMETRY_ENABLED);
305
- this.telemetryEnabled = envEnabled !== null && envEnabled !== void 0 ? envEnabled : DEFAULT_TELEMETRY_ENABLED;
305
+ telemetryEnabled = envEnabled !== null && envEnabled !== void 0 ? envEnabled : DEFAULT_TELEMETRY_ENABLED;
306
306
  }
307
307
  // Configure telemetryEnv: explicit option > env var > default ('PROD')
308
- if (this.telemetryEnabled) {
309
- this.telemetryEnv = getTelemetryEnv((_c = (_b = this.options.telemetry) === null || _b === void 0 ? void 0 : _b.env) !== null && _c !== void 0 ? _c : process.env.TELEMETRY_ENV);
308
+ let telemetryEnv = DEFAULT_TELEMETRY_ENV;
309
+ if (telemetryEnabled) {
310
+ 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
311
  }
312
+ return { telemetryEnabled, telemetryEnv };
311
313
  }
312
314
  /**
313
315
  * Override the SDK's `initialize` request handler to run mode resolution and
@@ -389,23 +391,10 @@ export class ActorsMcpServer {
389
391
  * survive a load that never sees an initialize.
390
392
  */
391
393
  isReportProblemServable() {
392
- return (!!this.telemetryEnabled &&
394
+ return (this.telemetryEnabled &&
393
395
  this.clientKnown &&
394
396
  !isReportProblemBlockedForClient(this.options.initializeRequestData));
395
397
  }
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
398
  composePendingToolsForClient() {
410
399
  if (this.pendingToolsUntilClientKnown.length === 0)
411
400
  return;
@@ -668,6 +657,24 @@ export class ActorsMcpServer {
668
657
  return {};
669
658
  });
670
659
  }
660
+ /**
661
+ * Token sources in order: per-request `_meta.apifyToken` (stdio inline) > server-instance
662
+ * option (set by the transport from `Authorization` header or stdio env). No env fallback:
663
+ * dev_server / production must extract the token from request headers so payment
664
+ * mode (no token) behaves identically to production.
665
+ */
666
+ resolveApifyToken(meta) {
667
+ return (meta === null || meta === void 0 ? void 0 : meta.apifyToken) || this.options.token;
668
+ }
669
+ /**
670
+ * Token-scoped client for resources/read (the API proxy needs auth). Deliberately token-only:
671
+ * unlike the CallTool path it does NOT forward provider/payment headers, so a payment-only
672
+ * session (x402/Skyfire, no Apify token) has no client and every read fails by design.
673
+ */
674
+ resolveApifyClient(params) {
675
+ const token = this.resolveApifyToken(params._meta);
676
+ return token ? new ApifyClient({ token }) : undefined;
677
+ }
671
678
  setupResourceHandlers() {
672
679
  const resourceService = createResourceService({
673
680
  paymentProvider: this.options.paymentProvider,
@@ -678,7 +685,7 @@ export class ActorsMcpServer {
678
685
  return await resourceService.listResources();
679
686
  });
680
687
  this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
681
- return await resourceService.readResource(request.params.uri);
688
+ return await resourceService.readResource(request.params.uri, this.resolveApifyClient(request.params));
682
689
  });
683
690
  this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
684
691
  return await resourceService.listResourceTemplates();
@@ -825,17 +832,12 @@ export class ActorsMcpServer {
825
832
  * @throws {McpError} - based on the McpServer class code from the typescript MCP SDK
826
833
  */
827
834
  this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
828
- var _a, _b, _c, _d, _e, _f;
835
+ var _a, _b, _c, _d, _e;
829
836
  const params = request.params;
830
837
  // eslint-disable-next-line prefer-const
831
838
  let { name, arguments: args, _meta: meta } = params;
832
839
  const progressToken = meta === null || meta === void 0 ? void 0 : meta.progressToken;
833
- const metaApifyToken = meta === null || meta === void 0 ? void 0 : meta.apifyToken;
834
- // Token sources in order: per-request `_meta.apifyToken` (stdio inline) > server-instance
835
- // option (set by the transport from `Authorization` header or stdio env). No env fallback:
836
- // dev_server / production must extract the token from request headers so payment
837
- // mode (no token) behaves identically to production.
838
- const apifyToken = (metaApifyToken || this.options.token);
840
+ const apifyToken = this.resolveApifyToken(meta);
839
841
  // mcpSessionId was injected upstream it is important and required for long running tasks as the store uses it and there is not other way to pass it
840
842
  const mcpSessionId = meta === null || meta === void 0 ? void 0 : meta.mcpSessionId;
841
843
  if (!mcpSessionId) {
@@ -857,7 +859,13 @@ export class ActorsMcpServer {
857
859
  // On a failed result, nudge the agent to report the blocker via report-problem at the
858
860
  // moment it decides what to do next. Gated on the tool actually being served (see
859
861
  // isReportProblemServable), so clients where it is blocklisted or telemetry is off never see it.
860
- const augmented = this.withReportProblemNudge(r, resolvedToolName, callDiagnostics.failure_category, callDiagnostics.failure_http_status);
862
+ const augmented = withReportProblemNudge({
863
+ result: r,
864
+ tools: this.tools,
865
+ failingToolName: resolvedToolName,
866
+ failureCategory: callDiagnostics.failure_category,
867
+ failureHttpStatus: callDiagnostics.failure_http_status,
868
+ });
861
869
  toolResult = augmented;
862
870
  return augmented;
863
871
  };
@@ -879,7 +887,12 @@ export class ActorsMcpServer {
879
887
  };
880
888
  // Initialize telemetry with raw tool name — updated below once the tool is resolved.
881
889
  // This ensures telemetry is available even for early failures (missing token, tool not found).
882
- const { telemetryData, userId } = await this.prepareTelemetryData(name, mcpSessionId, apifyToken);
890
+ const { telemetryData, userId } = await prepareTelemetryData({
891
+ toolName: name,
892
+ mcpSessionId,
893
+ apifyToken,
894
+ apifyMcpServer: this,
895
+ });
883
896
  // actorName/actorId are declared here so they're available in the catch block for telemetry.
884
897
  // Set after tool resolution (inside the try block).
885
898
  let actorName;
@@ -1077,196 +1090,75 @@ export class ActorsMcpServer {
1077
1090
  callDiagnostics = outcome.callDiagnostics;
1078
1091
  return captureResult(outcome.result);
1079
1092
  }
1080
- // Handle internal tool
1081
- if (tool.type === TOOL_TYPE.INTERNAL) {
1082
- // Tools that may emit notifications/progress during a sync wait must be opted in here.
1083
- // call-actor: emits during start+waitForFinish. get-actor-run: emits when waitSecs > 0.
1084
- const progressTrackerOptIn = tool.name === HELPER_TOOLS.ACTOR_CALL || tool.name === HELPER_TOOLS.ACTOR_RUNS_GET;
1085
- const progressTracker = progressTrackerOptIn
1086
- ? createProgressTracker(progressToken, extra.sendNotification)
1087
- : null;
1088
- try {
1089
- log.info('Calling internal tool', { toolName: tool.name, mcpSessionId, input: logSafeArgs });
1090
- const res = (await tool.call({
1091
- args: toolArgs,
1092
- extra,
1093
- apifyMcpServer: this,
1094
- mcpServer: this.server,
1095
- apifyToken,
1096
- apifyClient: apifyClient,
1097
- progressTracker,
1098
- mcpSessionId,
1099
- }));
1100
- // Extract diagnostics and strip internal fields from res before returning to client.
1101
- ({ toolStatus, callDiagnostics } = applyToolTelemetry(res, actorName, actorId, callDiagnostics));
1102
- return captureResult(res);
1103
- }
1104
- finally {
1105
- progressTracker === null || progressTracker === void 0 ? void 0 : progressTracker.stop();
1106
- }
1107
- }
1108
- if (tool.type === TOOL_TYPE.ACTOR_MCP) {
1109
- let client = null;
1110
- try {
1111
- client = await connectMCPClient(tool.serverUrl, apifyToken, mcpSessionId);
1112
- if (!client) {
1113
- const msg = dedent `
1114
- Failed to connect to MCP server at "${tool.serverUrl}".
1115
- Please verify the server URL is correct and accessible, and ensure you have a valid Apify token with appropriate permissions.
1116
- `;
1117
- log.softFail(msg, { mcpSessionId, failureCategory: FAILURE_CATEGORY.INTERNAL_ERROR });
1118
- await this.server.sendLoggingMessage({ level: 'error', data: msg });
1119
- toolStatus = TOOL_STATUS.SOFT_FAIL;
1120
- callDiagnostics = { ...callDiagnostics, failure_category: FAILURE_CATEGORY.INTERNAL_ERROR };
1121
- return captureResult(respondErrorNoTelemetry(msg));
1122
- }
1123
- // Only set up notification handlers if progressToken is provided by the client
1124
- if (progressToken !== undefined && progressToken !== null) {
1125
- // Set up notification handlers for the client
1126
- for (const schema of ServerNotificationSchema.options) {
1127
- const method = schema.shape.method.value;
1128
- // Forward notifications from the proxy client to the server
1129
- client.setNotificationHandler(schema, async (notification) => {
1130
- log.debug('Sending MCP notification', {
1131
- method,
1132
- mcpSessionId,
1133
- notification,
1134
- });
1135
- await extra.sendNotification(notification);
1136
- });
1137
- }
1138
- }
1139
- log.info('Calling Actor-MCP', {
1140
- toolName: tool.name,
1141
- actorMcpToolName: tool.originToolName,
1142
- actorId: tool.actorId,
1143
- mcpSessionId,
1144
- input: logSafeArgs,
1145
- });
1146
- const res = await client.callTool({
1147
- name: tool.originToolName,
1148
- arguments: toolArgs,
1149
- _meta: { progressToken },
1150
- }, CallToolResultSchema, {
1151
- timeout: EXTERNAL_TOOL_CALL_TIMEOUT_MSEC,
1152
- });
1153
- // TODO: actor-mcp responses are opaque — isError could be a user input problem
1154
- // (e.g. invalid query) or a genuine server failure. We can't distinguish without
1155
- // parsing the error text. Defaulting to INTERNAL_ERROR for now; revisit when
1156
- // actor-mcp gets deeper telemetry treatment.
1157
- if ('isError' in res && res.isError) {
1158
- toolStatus = TOOL_STATUS.SOFT_FAIL;
1159
- callDiagnostics = {
1160
- failure_category: FAILURE_CATEGORY.INTERNAL_ERROR,
1161
- ...buildActorFields(actorName, actorId),
1162
- };
1163
- }
1164
- return captureResult({ ...res });
1165
- }
1166
- catch (error) {
1167
- toolStatus = getToolStatusFromError(error, Boolean((_e = extra.signal) === null || _e === void 0 ? void 0 : _e.aborted));
1168
- const failureDetail = error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200);
1169
- callDiagnostics = {
1170
- failure_category: classifyFailureCategory(error),
1171
- failure_detail: failureDetail,
1172
- ...buildActorFields(actorName, actorId),
1173
- };
1174
- logHttpError(error, `Failed to call MCP tool '${tool.originToolName}' on Actor '${tool.actorId}'`, {
1175
- actorId: tool.actorId,
1176
- toolName: tool.originToolName,
1177
- failureCategory: callDiagnostics.failure_category,
1178
- });
1179
- return captureResult(respondErrorNoTelemetry(`Failed to call MCP tool '${tool.originToolName}' on Actor '${tool.actorId}': ${remoteMcpFailureDetail(error)}`));
1180
- }
1181
- finally {
1182
- if (client)
1183
- await client.close();
1184
- }
1185
- }
1186
- // Handle actor tool
1187
- if (tool.type === TOOL_TYPE.ACTOR) {
1188
- const progressTracker = createProgressTracker(progressToken, extra.sendNotification);
1189
- try {
1190
- log.info('Calling Actor', {
1191
- toolName: tool.name,
1192
- actorName: tool.actorFullName,
1193
- mcpSessionId,
1194
- input: logSafeArgs,
1195
- });
1196
- const executorResult = await actorExecutor.executeActorTool({
1197
- actorFullName: tool.actorFullName,
1198
- input: toolArgs,
1199
- apifyClient: apifyClient,
1200
- callOptions: { memory: tool.memoryMbytes },
1201
- progressTracker,
1202
- abortSignal: extra.signal,
1203
- mcpSessionId,
1204
- datasetItemsSchema: tool.datasetItemsSchema,
1205
- });
1206
- if (!executorResult) {
1207
- toolStatus = TOOL_STATUS.ABORTED;
1208
- // Receivers of cancellation notifications SHOULD NOT send a response for the cancelled request
1209
- // https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/cancellation#behavior-requirements
1210
- return captureResult({});
1211
- }
1212
- // Mirror the INTERNAL branch: read the telemetry the executor embedded on error
1213
- // results (e.g. respondUserError → SOFT_FAIL/INVALID_INPUT), strip it from the wire,
1214
- // and set failure_category so the report-problem nudge picks the softer variant.
1215
- ({ toolStatus, callDiagnostics } = applyToolTelemetry(executorResult, actorName, actorId, callDiagnostics));
1216
- return captureResult(executorResult);
1217
- }
1218
- finally {
1219
- if (progressTracker) {
1220
- progressTracker.stop();
1221
- }
1222
- }
1223
- }
1224
- // If we reached here without returning, it means the tool type was not recognized (user error)
1225
- 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 dispatchToolCall({
1103
+ tool,
1104
+ toolArgs: toolArgs,
1105
+ logSafeArgs,
1106
+ apifyToken,
1107
+ apifyClient: apifyClient,
1108
+ apifyMcpServer: this,
1109
+ mcpSessionId,
1110
+ progressToken,
1111
+ progressTracker,
1112
+ shouldForwardNotifications: true,
1113
+ extra,
1114
+ actorName,
1115
+ actorId,
1116
+ taskMode: false,
1117
+ });
1118
+ toolStatus = dispatchResult.toolStatus;
1119
+ callDiagnostics = dispatchResult.callDiagnostics;
1120
+ return captureResult(dispatchResult.result);
1226
1121
  }
1227
1122
  catch (error) {
1228
- const httpStatus = getHttpStatusCode(error);
1123
+ // Re-throw MCP protocol errors (e.g. from failInvalidParams) so the SDK
1124
+ // returns them as JSON-RPC errors. failInvalidParams already set callDiagnostics
1125
+ // with the correct semantic category (e.g. AUTH), so we must not overwrite it.
1126
+ // Re-throwing first is order-equivalent only because no McpError with an HTTP-range
1127
+ // code reaches this catch: an McpError(402) WOULD satisfy the x402 predicate
1128
+ // (getHttpStatusCode falls through to `.code`), but every remote-McpError route is
1129
+ // sealed by an inner catch (ACTOR_MCP branch, call-actor MCP passthrough) and all
1130
+ // in-repo McpErrors use negative ErrorCode.* values.
1131
+ if (error instanceof McpError) {
1132
+ throw error;
1133
+ }
1134
+ const errorResult = buildToolCallErrorResult(error, {
1135
+ toolName: name,
1136
+ actorName,
1137
+ actorId,
1138
+ isAborted: Boolean((_e = extra.signal) === null || _e === void 0 ? void 0 : _e.aborted),
1139
+ });
1140
+ toolStatus = errorResult.toolStatus;
1229
1141
  // Propagate 402 Payment Required as a tool result per x402 MCP transport spec:
1230
- // content[0].text (JSON) + isError: true. The concurrent-run limit falls through
1231
- // to the run-limit message below.
1232
- if (isX402PaymentRequiredError(error)) {
1233
- toolStatus = TOOL_STATUS.SOFT_FAIL;
1234
- callDiagnostics = {
1235
- failure_category: FAILURE_CATEGORY.INVALID_INPUT,
1236
- failure_http_status: 402,
1237
- ...buildActorFields(actorName, actorId),
1238
- };
1239
- return captureResult(buildPaymentRequiredResponse(error));
1142
+ // content[0].text (JSON) + isError: true. No log here (unlike the task path). The
1143
+ // concurrent-run limit also surfaces as 402 but is excluded by the predicate and
1144
+ // falls through to the generic run-limit handling below.
1145
+ if (errorResult.kind === TOOL_CALL_ERROR_KIND.PAYMENT) {
1146
+ callDiagnostics = errorResult.callDiagnostics;
1147
+ return captureResult(errorResult.response);
1240
1148
  }
1241
- if (isPermissionApprovalError(error)) {
1242
- toolStatus = TOOL_STATUS.SOFT_FAIL;
1243
- callDiagnostics = {
1244
- failure_category: FAILURE_CATEGORY.PERMISSION_APPROVAL_REQUIRED,
1245
- failure_http_status: error.statusCode,
1246
- ...buildActorFields(actorName, actorId),
1247
- };
1149
+ if (errorResult.kind === TOOL_CALL_ERROR_KIND.APPROVAL) {
1150
+ callDiagnostics = errorResult.callDiagnostics;
1248
1151
  logHttpError(error, 'Permission approval required while calling tool', {
1249
1152
  toolName: name,
1250
1153
  mcpSessionId,
1251
1154
  });
1252
- return captureResult(buildPermissionApprovalResponse(error));
1155
+ return captureResult(errorResult.response);
1253
1156
  }
1254
- // Re-throw MCP protocol errors (e.g. from failInvalidParams) so the SDK
1255
- // returns them as JSON-RPC errors. failInvalidParams already set callDiagnostics
1256
- // with the correct semantic category (e.g. AUTH), so we must not overwrite it.
1257
- if (error instanceof McpError) {
1258
- throw error;
1259
- }
1260
- toolStatus = getToolStatusFromError(error, Boolean((_f = extra.signal) === null || _f === void 0 ? void 0 : _f.aborted));
1261
- const failureDetail = error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200);
1262
1157
  callDiagnostics = {
1263
1158
  // Spread existing diagnostics first (e.g. validation_keyword from failInvalidParams),
1264
- // then overwrite with freshly computed fields so they take precedence.
1159
+ // then overwrite with the mapper's freshly computed fields so they take precedence.
1265
1160
  ...callDiagnostics,
1266
- failure_category: classifyFailureCategory(error),
1267
- ...(httpStatus !== undefined ? { failure_http_status: httpStatus } : {}),
1268
- failure_detail: failureDetail,
1269
- ...buildActorFields(actorName, actorId),
1161
+ ...errorResult.callDiagnostics,
1270
1162
  };
1271
1163
  logHttpError(error, 'Error occurred while calling tool', {
1272
1164
  toolName: name,
@@ -1285,14 +1177,14 @@ export class ActorsMcpServer {
1285
1177
  // reuse the local ABORTED-aware toolStatus, do NOT re-derive from the error (which
1286
1178
  // would drop ABORTED and leak failureCategory/failureHttpStatus onto the wire).
1287
1179
  return captureResult({
1288
- ...respondOk(getToolCallErrorUserText(name, error)),
1180
+ ...respondOk(errorResult.userText),
1289
1181
  isError: true,
1290
1182
  toolTelemetry: { toolStatus },
1291
1183
  });
1292
1184
  }
1293
1185
  finally {
1294
1186
  if (shouldTrackTelemetry) {
1295
- this.logToolCallAndTelemetry({
1187
+ logToolCallAndTelemetry({
1296
1188
  toolName: resolvedToolName,
1297
1189
  mcpSessionId,
1298
1190
  toolStatus,
@@ -1303,73 +1195,13 @@ export class ActorsMcpServer {
1303
1195
  args,
1304
1196
  result: toolResult,
1305
1197
  taskId: preflightTaskId,
1198
+ apifyMcpServer: this,
1306
1199
  });
1307
1200
  }
1308
1201
  }
1309
- const availableTools = this.listToolNames();
1310
- const msg = dedent `
1311
- Unknown tool type for "${name}".
1312
- Available tools: ${availableTools.length > 0 ? availableTools.join(', ') : 'none'}.
1313
- Please verify the tool name and ensure the tool is properly registered.
1314
- `;
1315
- log.softFail(msg, { mcpSessionId, statusCode: 404 });
1316
- await this.server.sendLoggingMessage({
1317
- level: 'error',
1318
- data: msg,
1319
- });
1320
- throw new McpError(ErrorCode.InvalidParams, msg);
1321
1202
  });
1322
1203
  }
1323
- /**
1324
- * Logs tool call completion at INFO level and tracks telemetry.
1325
- * Computes duration once so both the log line and telemetry event use the same value.
1326
- * Response bytes and resource ids are derived here from the raw `result` (+ `args`) so every
1327
- * call site stays a plain hand-off — no path can forget to compute or strip them.
1328
- */
1329
- logToolCallAndTelemetry(params) {
1330
- const durationMs = Date.now() - params.startTime;
1331
- // `result` is undefined only on short-circuit paths that never produced a payload (e.g. a
1332
- // cancelled task); skip byte accounting there. `null`/`{}` still measure as zero bytes.
1333
- const responseBytes = params.result === undefined ? undefined : computeToolResponseBytes(params.result);
1334
- log.info('Tool call completed', {
1335
- toolName: params.toolName,
1336
- mcpSessionId: params.mcpSessionId,
1337
- toolStatus: params.toolStatus,
1338
- durationMs,
1339
- ...(responseBytes !== undefined && {
1340
- responseContentBytes: responseBytes.contentBytes,
1341
- responseStructuredContentBytes: responseBytes.structuredContentBytes,
1342
- responseFileBytes: responseBytes.fileBytes,
1343
- }),
1344
- ...(params.taskId !== undefined && { taskId: params.taskId }),
1345
- });
1346
- if (params.telemetryData) {
1347
- const finalizedTelemetryData = {
1348
- ...params.telemetryData,
1349
- tool_status: params.toolStatus,
1350
- tool_exec_time_ms: durationMs,
1351
- ...(responseBytes && {
1352
- tool_response_content_bytes: responseBytes.contentBytes,
1353
- tool_response_structured_content_bytes: responseBytes.structuredContentBytes,
1354
- tool_response_file_bytes: responseBytes.fileBytes,
1355
- }),
1356
- // Always include actor_name/actor_id; failure-specific fields are only present when callDiagnostics has them.
1357
- ...params.callDiagnostics,
1358
- // Resource ids are read once here from the args + the tool's public output; no tool
1359
- // threads them back. Applied uniformly, last. See deriveResourceIds.
1360
- ...deriveResourceIds(params.args, params.result),
1361
- };
1362
- trackToolCall(params.userId, this.telemetryEnv, finalizedTelemetryData);
1363
- // A successful report-problem call also emits a dedicated feedback event carrying the
1364
- // submission. A downstream Segment destination fans it out to Slack/GitHub.
1365
- if (params.toolName === HELPER_TOOLS.PROBLEM_REPORT &&
1366
- params.toolStatus === TOOL_STATUS.SUCCEEDED &&
1367
- params.args) {
1368
- trackReportedProblem(params.userId, this.telemetryEnv, buildReportedProblemProperties(finalizedTelemetryData, params.args));
1369
- }
1370
- }
1371
- }
1372
- // TODO: this function quite duplicates the main tool call login the CallToolRequestSchema handler, we should refactor
1204
+ // 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.
1373
1205
  /**
1374
1206
  * Executes a tool asynchronously for a long-running task and updates task status.
1375
1207
  *
@@ -1395,9 +1227,14 @@ export class ActorsMcpServer {
1395
1227
  });
1396
1228
  // Prepare telemetry before try-catch so it's accessible to both paths.
1397
1229
  // This avoids re-fetching user data in the error handler.
1398
- const { telemetryData, userId } = await this.prepareTelemetryData(getToolFullName(tool), mcpSessionId, apifyToken);
1230
+ const { telemetryData, userId } = await prepareTelemetryData({
1231
+ toolName: getToolFullName(tool),
1232
+ mcpSessionId,
1233
+ apifyToken,
1234
+ apifyMcpServer: this,
1235
+ });
1399
1236
  const finishTaskTracking = (status, diagnostics, result) => {
1400
- this.logToolCallAndTelemetry({
1237
+ logToolCallAndTelemetry({
1401
1238
  toolName: tool.name,
1402
1239
  mcpSessionId,
1403
1240
  toolStatus: status,
@@ -1408,8 +1245,16 @@ export class ActorsMcpServer {
1408
1245
  callDiagnostics: diagnostics,
1409
1246
  args: toolArgs,
1410
1247
  result,
1248
+ apifyMcpServer: this,
1411
1249
  });
1412
1250
  };
1251
+ // Terminal step shared by every task outcome: store the result (expiry-tolerant),
1252
+ // emit the status notification, record telemetry.
1253
+ const completeTask = async (storeStatus, taskResult, status, diagnostics) => {
1254
+ await storeTaskResultOrSkipIfExpired(this.taskStore, tool.name, taskId, storeStatus, taskResult, mcpSessionId);
1255
+ await emitTaskStatusNotification(taskId, mcpSessionId, this.taskStore, this.server);
1256
+ finishTaskTracking(status, diagnostics, taskResult);
1257
+ };
1413
1258
  // Once a task is cancelled the spec forbids writing a result; every storage path
1414
1259
  // must short-circuit here. `logSuffix` is concatenated after "Task was cancelled"
1415
1260
  // so we keep the existing log format and the existing telemetry status per path.
@@ -1462,92 +1307,51 @@ export class ActorsMcpServer {
1462
1307
  await this.taskStore.updateTaskStatus(taskId, 'working', message, mcpSessionId);
1463
1308
  await emitTaskStatusNotification(taskId, mcpSessionId, this.taskStore, this.server);
1464
1309
  };
1465
- // Handle internal tool execution in task mode
1466
- if (toolStatus === TOOL_STATUS.SUCCEEDED && tool.type === TOOL_TYPE.INTERNAL) {
1467
- const progressTracker = createProgressTracker(progressToken, extra.sendNotification, taskId, onStatusMessage);
1468
- try {
1469
- log.info('Calling internal tool for task', {
1470
- taskId,
1471
- toolName: tool.name,
1472
- mcpSessionId,
1473
- input: logSafeArgs,
1474
- });
1475
- const res = (await tool.call({
1476
- args: toolArgs,
1477
- extra: taskExtra,
1478
- apifyMcpServer: this,
1479
- mcpServer: this.server,
1480
- apifyToken,
1481
- apifyClient,
1482
- progressTracker,
1483
- mcpSessionId,
1484
- taskMode: true,
1485
- }));
1486
- ({ toolStatus, callDiagnostics } = applyToolTelemetry(res, actorName, actorId, callDiagnostics));
1487
- result = res;
1488
- }
1489
- finally {
1490
- if (progressTracker) {
1491
- progressTracker.stop();
1492
- }
1493
- }
1494
- }
1495
- // Handle actor tool execution in task mode
1496
- if (toolStatus === TOOL_STATUS.SUCCEEDED && tool.type === TOOL_TYPE.ACTOR) {
1497
- const progressTracker = createProgressTracker(progressToken, extra.sendNotification, taskId, onStatusMessage);
1498
- try {
1499
- log.info('Calling Actor for task', {
1500
- taskId,
1501
- toolName: tool.name,
1502
- actorName: tool.actorFullName,
1503
- mcpSessionId,
1504
- input: logSafeArgs,
1505
- });
1506
- const executorResult = await actorExecutor.executeActorTool({
1507
- actorFullName: tool.actorFullName,
1508
- input: toolArgs,
1509
- apifyClient,
1510
- callOptions: { memory: tool.memoryMbytes },
1511
- progressTracker,
1512
- abortSignal: cancelWatcher.signal,
1513
- mcpSessionId,
1514
- datasetItemsSchema: tool.datasetItemsSchema,
1515
- taskMode: true,
1516
- });
1517
- if (!executorResult) {
1518
- toolStatus = TOOL_STATUS.ABORTED;
1519
- // Receivers of cancellation notifications SHOULD NOT send a response for the cancelled request
1520
- // https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/cancellation#behavior-requirements
1521
- result = {};
1522
- }
1523
- else {
1524
- // Mirror the INTERNAL task branch: extract embedded telemetry so failure_category
1525
- // drives the success-path nudge below and toolTelemetry is stripped from the wire.
1526
- ({ toolStatus, callDiagnostics } = applyToolTelemetry(executorResult, actorName, actorId, callDiagnostics));
1527
- result = executorResult;
1528
- }
1529
- }
1530
- finally {
1531
- if (progressTracker) {
1532
- progressTracker.stop();
1533
- }
1534
- }
1535
- }
1310
+ // ACTOR_MCP never reads the tracker (matching the sync path, which passes null for it);
1311
+ // INTERNAL/ACTOR get one built from taskId + onStatusMessage, which dispatch consumes
1312
+ // and stops.
1313
+ const progressTracker = tool.type === TOOL_TYPE.ACTOR_MCP
1314
+ ? null
1315
+ : createProgressTracker(progressToken, extra.sendNotification, taskId, onStatusMessage);
1316
+ const dispatchResult = await dispatchToolCall({
1317
+ tool,
1318
+ toolArgs,
1319
+ logSafeArgs,
1320
+ apifyToken,
1321
+ apifyClient,
1322
+ apifyMcpServer: this,
1323
+ mcpSessionId,
1324
+ progressToken,
1325
+ progressTracker,
1326
+ shouldForwardNotifications: false,
1327
+ extra: taskExtra,
1328
+ actorName,
1329
+ actorId,
1330
+ taskMode: true,
1331
+ logContext: { messageSuffix: ' for task', fields: { taskId } },
1332
+ });
1333
+ result = dispatchResult.result;
1334
+ toolStatus = dispatchResult.toolStatus;
1335
+ callDiagnostics = dispatchResult.callDiagnostics;
1536
1336
  // Check if task was cancelled before storing result
1537
1337
  if (await skipIfTaskCancelled(', skipping result storage', toolStatus))
1538
1338
  return;
1539
1339
  // On a failed result, nudge the agent to report the blocker via report-problem (mirrors the
1540
1340
  // synchronous CallTool path, which task-mode calls like call-actor bypass).
1541
- result = this.withReportProblemNudge(result, tool.name, callDiagnostics.failure_category, callDiagnostics.failure_http_status);
1341
+ result = withReportProblemNudge({
1342
+ result,
1343
+ tools: this.tools,
1344
+ failingToolName: tool.name,
1345
+ failureCategory: callDiagnostics.failure_category,
1346
+ failureHttpStatus: callDiagnostics.failure_http_status,
1347
+ });
1542
1348
  // Store the result in the task store
1543
1349
  log.debug('[executeToolAndUpdateTask] Storing completed result', {
1544
1350
  taskId,
1545
1351
  mcpSessionId,
1546
1352
  });
1547
- await storeTaskResultOrSkipIfExpired(this.taskStore, tool.name, taskId, 'completed', result, mcpSessionId);
1353
+ await completeTask('completed', result, toolStatus, callDiagnostics);
1548
1354
  log.debug('Task completed successfully', { taskId, toolName: tool.name, mcpSessionId });
1549
- await emitTaskStatusNotification(taskId, mcpSessionId, this.taskStore, this.server);
1550
- finishTaskTracking(toolStatus, callDiagnostics, result);
1551
1355
  }
1552
1356
  catch (error) {
1553
1357
  // Reached only when the task expired before the `working` transition (updateTaskStatus
@@ -1566,9 +1370,14 @@ export class ActorsMcpServer {
1566
1370
  });
1567
1371
  return;
1568
1372
  }
1373
+ const errorResult = buildToolCallErrorResult(error, {
1374
+ toolName: tool.name,
1375
+ actorName,
1376
+ actorId,
1377
+ isAborted: Boolean(cancelWatcher.signal.aborted),
1378
+ });
1569
1379
  // Handle 402 Payment Required — return structured x402 result so clients can auto-pay
1570
- const httpStatus = getHttpStatusCode(error);
1571
- if (isX402PaymentRequiredError(error)) {
1380
+ if (errorResult.kind === TOOL_CALL_ERROR_KIND.PAYMENT) {
1572
1381
  logHttpError(error, 'Payment required while calling tool (task mode)', { toolName: tool.name });
1573
1382
  // Per MCP tasks spec: once a task is cancelled it MUST remain cancelled,
1574
1383
  // so guard storeTaskResult against a cancel that raced with this 402.
@@ -1576,17 +1385,10 @@ export class ActorsMcpServer {
1576
1385
  ...buildActorFields(actorName, actorId),
1577
1386
  }))
1578
1387
  return;
1579
- const paymentResponse = buildPaymentRequiredResponse(error);
1580
- await storeTaskResultOrSkipIfExpired(this.taskStore, tool.name, taskId, 'completed', paymentResponse, mcpSessionId);
1581
- await emitTaskStatusNotification(taskId, mcpSessionId, this.taskStore, this.server);
1582
- finishTaskTracking(TOOL_STATUS.SOFT_FAIL, {
1583
- failure_category: FAILURE_CATEGORY.INVALID_INPUT,
1584
- failure_http_status: 402,
1585
- ...buildActorFields(actorName, actorId),
1586
- }, paymentResponse);
1388
+ await completeTask('completed', errorResult.response, errorResult.toolStatus, errorResult.callDiagnostics);
1587
1389
  return;
1588
1390
  }
1589
- if (isPermissionApprovalError(error)) {
1391
+ if (errorResult.kind === TOOL_CALL_ERROR_KIND.APPROVAL) {
1590
1392
  logHttpError(error, 'Permission approval required while calling tool (task mode)', {
1591
1393
  toolName: tool.name,
1592
1394
  });
@@ -1596,24 +1398,11 @@ export class ActorsMcpServer {
1596
1398
  ...buildActorFields(actorName, actorId),
1597
1399
  }))
1598
1400
  return;
1599
- const approvalResponse = buildPermissionApprovalResponse(error);
1600
- await storeTaskResultOrSkipIfExpired(this.taskStore, tool.name, taskId, 'completed', approvalResponse, mcpSessionId);
1601
- await emitTaskStatusNotification(taskId, mcpSessionId, this.taskStore, this.server);
1602
- finishTaskTracking(TOOL_STATUS.SOFT_FAIL, {
1603
- failure_category: FAILURE_CATEGORY.PERMISSION_APPROVAL_REQUIRED,
1604
- failure_http_status: error.statusCode,
1605
- ...buildActorFields(actorName, actorId),
1606
- }, approvalResponse);
1401
+ await completeTask('completed', errorResult.response, errorResult.toolStatus, errorResult.callDiagnostics);
1607
1402
  return;
1608
1403
  }
1609
- toolStatus = getToolStatusFromError(error, Boolean(cancelWatcher.signal.aborted));
1610
- const failureDetail = error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200);
1611
- callDiagnostics = {
1612
- failure_category: classifyFailureCategory(error),
1613
- ...(httpStatus !== undefined ? { failure_http_status: httpStatus } : {}),
1614
- failure_detail: failureDetail,
1615
- ...buildActorFields(actorName, actorId),
1616
- };
1404
+ toolStatus = errorResult.toolStatus;
1405
+ callDiagnostics = errorResult.callDiagnostics;
1617
1406
  // Log level follows the already-classified toolStatus:
1618
1407
  // SOFT_FAIL (e.g. 402/403 user quota, client-side issues) → softFail
1619
1408
  // FAILED/ABORTED/other → error
@@ -1643,7 +1432,7 @@ export class ActorsMcpServer {
1643
1432
  error,
1644
1433
  });
1645
1434
  }
1646
- const userText = getToolCallErrorUserText(tool.name, error);
1435
+ const { userText } = errorResult;
1647
1436
  // Check if task was cancelled before storing result
1648
1437
  if (await skipIfTaskCancelled(', skipping result storage', toolStatus, callDiagnostics))
1649
1438
  return;
@@ -1651,60 +1440,33 @@ export class ActorsMcpServer {
1651
1440
  taskId,
1652
1441
  mcpSessionId,
1653
1442
  });
1654
- // Nudge on a genuinely-failed task result. INTERNAL_ERROR and an unknown category get the
1655
- // full nudge; a genuine INVALID_INPUT gets the softer nudge; payment (402) is suppressed via
1656
- // the HTTP status and AUTH / PERMISSION_APPROVAL_REQUIRED via NON_NUDGE_FAILURE_CATEGORIES.
1657
- // The 402 branch above returns before reaching here, so this only sees non-payment failures.
1658
- const failedResult = this.withReportProblemNudge({
1659
- content: [
1660
- {
1661
- type: 'text',
1662
- text: userText,
1663
- },
1664
- ],
1665
- isError: true,
1666
- internalToolStatus: toolStatus,
1667
- }, tool.name, callDiagnostics.failure_category, callDiagnostics.failure_http_status);
1668
- await storeTaskResultOrSkipIfExpired(this.taskStore, tool.name, taskId, 'failed', failedResult, mcpSessionId);
1669
- await emitTaskStatusNotification(taskId, mcpSessionId, this.taskStore, this.server);
1670
- finishTaskTracking(toolStatus, callDiagnostics, failedResult);
1443
+ // Nudge on a genuinely-failed task result (mirrors the completed path and the sync
1444
+ // captureResult). INTERNAL_ERROR and an unknown category get the full nudge; a genuine
1445
+ // INVALID_INPUT gets the softer nudge; payment (402) is suppressed via the HTTP status and
1446
+ // AUTH / PERMISSION_APPROVAL_REQUIRED via NON_NUDGE_FAILURE_CATEGORIES. The 402 branch above
1447
+ // returns before reaching here, so this only sees non-payment failures.
1448
+ const failedResult = withReportProblemNudge({
1449
+ result: {
1450
+ content: [
1451
+ {
1452
+ type: 'text',
1453
+ text: userText,
1454
+ },
1455
+ ],
1456
+ isError: true,
1457
+ internalToolStatus: toolStatus,
1458
+ },
1459
+ tools: this.tools,
1460
+ failingToolName: tool.name,
1461
+ failureCategory: callDiagnostics.failure_category,
1462
+ failureHttpStatus: callDiagnostics.failure_http_status,
1463
+ });
1464
+ await completeTask('failed', failedResult, toolStatus, callDiagnostics);
1671
1465
  }
1672
1466
  finally {
1673
1467
  cancelWatcher.dispose();
1674
1468
  }
1675
1469
  }
1676
- /*
1677
- * Creates telemetry data for a tool call.
1678
- */
1679
- async prepareTelemetryData(toolName, mcpSessionId, apifyToken) {
1680
- var _a, _b, _c, _d, _e;
1681
- if (!this.telemetryEnabled) {
1682
- return { telemetryData: null, userId: null };
1683
- }
1684
- // Get userId from cache or fetch from API
1685
- let userId = null;
1686
- if (apifyToken) {
1687
- const apifyClient = new ApifyClient({ token: apifyToken });
1688
- ({ userId } = await getUserInfoCached(apifyToken, apifyClient));
1689
- log.debug('Telemetry: fetched userId', { userId, mcpSessionId });
1690
- }
1691
- const capabilities = (_b = (_a = this.options.initializeRequestData) === null || _a === void 0 ? void 0 : _a.params) === null || _b === void 0 ? void 0 : _b.capabilities;
1692
- const params = (_c = this.options.initializeRequestData) === null || _c === void 0 ? void 0 : _c.params;
1693
- const telemetryData = {
1694
- app: 'mcp',
1695
- app_version: getPackageVersion() || '',
1696
- mcp_client_name: ((_d = params === null || params === void 0 ? void 0 : params.clientInfo) === null || _d === void 0 ? void 0 : _d.name) || '',
1697
- mcp_client_version: ((_e = params === null || params === void 0 ? void 0 : params.clientInfo) === null || _e === void 0 ? void 0 : _e.version) || '',
1698
- mcp_protocol_version: (params === null || params === void 0 ? void 0 : params.protocolVersion) || '',
1699
- mcp_client_capabilities: capabilities || null,
1700
- mcp_session_id: mcpSessionId || '',
1701
- transport_type: this.options.transportType || '',
1702
- tool_name: toolName,
1703
- tool_status: TOOL_STATUS.SUCCEEDED, // Will be updated in finally
1704
- tool_exec_time_ms: 0, // Will be calculated in finally
1705
- };
1706
- return { telemetryData, userId };
1707
- }
1708
1470
  /**
1709
1471
  * Resolves widgets and determines which ones are ready to be served.
1710
1472
  */