@apify/actors-mcp-server 0.11.6-beta.1 → 0.11.6-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.
Files changed (129) hide show
  1. package/README.md +4 -0
  2. package/dist/const.d.ts +10 -0
  3. package/dist/const.d.ts.map +1 -1
  4. package/dist/const.js +10 -0
  5. package/dist/const.js.map +1 -1
  6. package/dist/dev_server.d.ts.map +1 -1
  7. package/dist/dev_server.js +4 -5
  8. package/dist/dev_server.js.map +1 -1
  9. package/dist/index_internals.d.ts +1 -0
  10. package/dist/index_internals.d.ts.map +1 -1
  11. package/dist/mcp/actors.d.ts +0 -4
  12. package/dist/mcp/actors.d.ts.map +1 -1
  13. package/dist/mcp/actors.js +1 -7
  14. package/dist/mcp/actors.js.map +1 -1
  15. package/dist/mcp/server.d.ts +63 -10
  16. package/dist/mcp/server.d.ts.map +1 -1
  17. package/dist/mcp/server.js +236 -123
  18. package/dist/mcp/server.js.map +1 -1
  19. package/dist/resources/resource_service.d.ts.map +1 -1
  20. package/dist/resources/resource_service.js +2 -2
  21. package/dist/resources/resource_service.js.map +1 -1
  22. package/dist/resources/widgets.d.ts.map +1 -1
  23. package/dist/resources/widgets.js +5 -5
  24. package/dist/resources/widgets.js.map +1 -1
  25. package/dist/stdio.js +2 -7
  26. package/dist/stdio.js.map +1 -1
  27. package/dist/telemetry.d.ts +14 -1
  28. package/dist/telemetry.d.ts.map +1 -1
  29. package/dist/telemetry.js +41 -0
  30. package/dist/telemetry.js.map +1 -1
  31. package/dist/tools/actors/actor_run_response.d.ts +1 -1
  32. package/dist/tools/actors/actor_run_response.d.ts.map +1 -1
  33. package/dist/tools/actors/call_actor.d.ts +4 -4
  34. package/dist/tools/actors/call_actor.d.ts.map +1 -1
  35. package/dist/tools/actors/call_actor.js +6 -6
  36. package/dist/tools/actors/call_actor.js.map +1 -1
  37. package/dist/tools/actors/search_actors.d.ts +11 -0
  38. package/dist/tools/actors/search_actors.d.ts.map +1 -1
  39. package/dist/tools/actors/search_actors.js +29 -21
  40. package/dist/tools/actors/search_actors.js.map +1 -1
  41. package/dist/tools/dev/report_problem.d.ts +40 -0
  42. package/dist/tools/dev/report_problem.d.ts.map +1 -0
  43. package/dist/tools/dev/report_problem.js +112 -0
  44. package/dist/tools/dev/report_problem.js.map +1 -0
  45. package/dist/tools/registry.d.ts +1 -1
  46. package/dist/tools/registry.d.ts.map +1 -1
  47. package/dist/tools/registry.js +2 -1
  48. package/dist/tools/registry.js.map +1 -1
  49. package/dist/tools/runs/get_actor_run.d.ts.map +1 -1
  50. package/dist/tools/runs/get_actor_run.js +2 -2
  51. package/dist/tools/runs/get_actor_run.js.map +1 -1
  52. package/dist/tools/runs/get_actor_run_list.d.ts.map +1 -1
  53. package/dist/tools/runs/get_actor_run_list.js +5 -2
  54. package/dist/tools/runs/get_actor_run_list.js.map +1 -1
  55. package/dist/tools/runs/get_actor_run_log.d.ts.map +1 -1
  56. package/dist/tools/runs/get_actor_run_log.js +2 -1
  57. package/dist/tools/runs/get_actor_run_log.js.map +1 -1
  58. package/dist/tools/storage/get_dataset_items.d.ts.map +1 -1
  59. package/dist/tools/storage/get_dataset_items.js +5 -17
  60. package/dist/tools/storage/get_dataset_items.js.map +1 -1
  61. package/dist/tools/storage/get_dataset_list.d.ts.map +1 -1
  62. package/dist/tools/storage/get_dataset_list.js +0 -1
  63. package/dist/tools/storage/get_dataset_list.js.map +1 -1
  64. package/dist/tools/storage/get_dataset_schema.d.ts.map +1 -1
  65. package/dist/tools/storage/get_dataset_schema.js +3 -14
  66. package/dist/tools/storage/get_dataset_schema.js.map +1 -1
  67. package/dist/tools/storage/get_key_value_store_keys.d.ts.map +1 -1
  68. package/dist/tools/storage/get_key_value_store_keys.js +4 -14
  69. package/dist/tools/storage/get_key_value_store_keys.js.map +1 -1
  70. package/dist/tools/storage/get_key_value_store_list.d.ts.map +1 -1
  71. package/dist/tools/storage/get_key_value_store_list.js +0 -1
  72. package/dist/tools/storage/get_key_value_store_list.js.map +1 -1
  73. package/dist/tools/storage/get_key_value_store_record.js +9 -9
  74. package/dist/tools/storage/get_key_value_store_record.js.map +1 -1
  75. package/dist/tools/storage/storage_helpers.d.ts +14 -17
  76. package/dist/tools/storage/storage_helpers.d.ts.map +1 -1
  77. package/dist/tools/storage/storage_helpers.js +27 -11
  78. package/dist/tools/storage/storage_helpers.js.map +1 -1
  79. package/dist/tools/widgets/get_actor_run_widget.d.ts.map +1 -1
  80. package/dist/tools/widgets/get_actor_run_widget.js +2 -1
  81. package/dist/tools/widgets/get_actor_run_widget.js.map +1 -1
  82. package/dist/tools/widgets/search_actors_widget.d.ts.map +1 -1
  83. package/dist/tools/widgets/search_actors_widget.js +2 -6
  84. package/dist/tools/widgets/search_actors_widget.js.map +1 -1
  85. package/dist/tsconfig.tsbuildinfo +1 -1
  86. package/dist/types.d.ts +13 -1
  87. package/dist/types.d.ts.map +1 -1
  88. package/dist/types.js.map +1 -1
  89. package/dist/utils/actor.d.ts +1 -15
  90. package/dist/utils/actor.d.ts.map +1 -1
  91. package/dist/utils/actor.js +0 -62
  92. package/dist/utils/actor.js.map +1 -1
  93. package/dist/utils/encode_text.d.ts +0 -30
  94. package/dist/utils/encode_text.d.ts.map +1 -1
  95. package/dist/utils/encode_text.js +1 -79
  96. package/dist/utils/encode_text.js.map +1 -1
  97. package/dist/utils/generic.d.ts +0 -17
  98. package/dist/utils/generic.d.ts.map +1 -1
  99. package/dist/utils/generic.js +0 -49
  100. package/dist/utils/generic.js.map +1 -1
  101. package/dist/utils/mcp.d.ts +40 -24
  102. package/dist/utils/mcp.d.ts.map +1 -1
  103. package/dist/utils/mcp.js +31 -0
  104. package/dist/utils/mcp.js.map +1 -1
  105. package/dist/utils/mcp_clients.d.ts +6 -2
  106. package/dist/utils/mcp_clients.d.ts.map +1 -1
  107. package/dist/utils/mcp_clients.js +9 -16
  108. package/dist/utils/mcp_clients.js.map +1 -1
  109. package/dist/utils/payment_errors.d.ts +1 -10
  110. package/dist/utils/payment_errors.d.ts.map +1 -1
  111. package/dist/utils/server-instructions/index.d.ts +5 -2
  112. package/dist/utils/server-instructions/index.d.ts.map +1 -1
  113. package/dist/utils/server-instructions/index.js +10 -3
  114. package/dist/utils/server-instructions/index.js.map +1 -1
  115. package/dist/utils/tool_status.d.ts +10 -0
  116. package/dist/utils/tool_status.d.ts.map +1 -1
  117. package/dist/utils/tool_status.js +10 -0
  118. package/dist/utils/tool_status.js.map +1 -1
  119. package/dist/utils/tools.d.ts +0 -10
  120. package/dist/utils/tools.d.ts.map +1 -1
  121. package/dist/utils/tools.js +0 -35
  122. package/dist/utils/tools.js.map +1 -1
  123. package/dist/utils/tools_loader.d.ts.map +1 -1
  124. package/dist/utils/tools_loader.js +6 -0
  125. package/dist/utils/tools_loader.js.map +1 -1
  126. package/dist/web/dist/actor-detail-widget.js +171 -462
  127. package/dist/web/dist/actor-run-widget.js +189 -480
  128. package/dist/web/dist/search-actors-widget.js +168 -459
  129. package/package.json +1 -3
@@ -19,21 +19,23 @@ 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 { getTelemetryEnv, trackToolCall } from '../telemetry.js';
22
+ import { buildReportedProblemProperties, getTelemetryEnv, trackReportedProblem, trackToolCall } from '../telemetry.js';
23
23
  import { decodeDotPropertyNames } from '../tools/actor_input_schema.js';
24
24
  import { legacyToolNameToNew } from '../tools/actor_tool_naming.js';
25
25
  import { actorExecutor } from '../tools/actors/actor_executor.js';
26
26
  import { buildPermissionApprovalResponse, checkPaymentProviderStandbyConflict } from '../tools/actors/call_actor.js';
27
+ import { appendReportProblemNudge } from '../tools/dev/report_problem.js';
27
28
  import { getActorsAsTools } from '../tools/index.js';
28
29
  import { SERVER_MODE, TOOL_TYPE } from '../types.js';
29
30
  import { isPermissionApprovalError, remoteMcpFailureDetail } from '../utils/apify_errors.js';
30
31
  import { getHttpStatusCode, isMcpClientFaultMessage, logHttpError, sanitizeMezmoMessage } from '../utils/logging.js';
31
32
  import { computeToolResponseBytes, getToolCallErrorUserText, respondErrorNoTelemetry, respondOk, } from '../utils/mcp.js';
33
+ import { isReportProblemBlockedForClient } from '../utils/mcp_clients.js';
32
34
  import { buildPaymentRequiredResponse, isX402PaymentRequiredError } from '../utils/payment_errors.js';
33
35
  import { createProgressTracker } from '../utils/progress.js';
34
36
  import { getServerInstructions } from '../utils/server-instructions/index.js';
35
37
  import { parseServerMode, resolveServerMode } from '../utils/server_mode.js';
36
- import { classifyFailureCategory, deriveResourceIds, extractAjvErrorDetails, extractToolTelemetry, getToolStatusFromError, } from '../utils/tool_status.js';
38
+ import { applyToolTelemetry, classifyFailureCategory, deriveResourceIds, extractAjvErrorDetails, getToolStatusFromError, } from '../utils/tool_status.js';
37
39
  import { buildActorFields, extractActorId, extractActorName, getToolFullName, getToolPublicFieldOnly, } from '../utils/tools.js';
38
40
  import { getActors, getToolsForServerMode, toolNamesToInput } from '../utils/tools_loader.js';
39
41
  import { getUserInfoCached } from '../utils/userid_cache.js';
@@ -81,6 +83,24 @@ export async function emitTaskStatusNotification(taskId, mcpSessionId, taskStore
81
83
  // Silent fail — notifications are advisory
82
84
  }
83
85
  }
86
+ /**
87
+ * Shared diagnostics for a pre-flight failure (standby-provider conflict or missing/invalid payment
88
+ * signature) — a call outcome already known before any Actor runs. Standby rejection wins over the
89
+ * generic payment-required failure so the agent gets the precise reason instead of a generic 402.
90
+ * Pure: callers own their own post-handling (return the result directly, or store + notify + synthesize
91
+ * a terminal task) — call this only once `standbyRejection ?? paymentRequiredResult` is already truthy.
92
+ */
93
+ function buildPreflightFailureOutcome(standbyRejection, paymentRequiredResult, actorName, actorId) {
94
+ return {
95
+ toolStatus: TOOL_STATUS.SOFT_FAIL,
96
+ callDiagnostics: {
97
+ failure_category: FAILURE_CATEGORY.INVALID_INPUT,
98
+ ...(standbyRejection ? {} : { failure_http_status: 402 }),
99
+ ...buildActorFields(actorName, actorId),
100
+ },
101
+ result: (standbyRejection !== null && standbyRejection !== void 0 ? standbyRejection : paymentRequiredResult),
102
+ };
103
+ }
84
104
  /**
85
105
  * Create Apify MCP server
86
106
  */
@@ -156,7 +176,10 @@ export class ActorsMcpServer {
156
176
  writable: true,
157
177
  value: void 0
158
178
  });
159
- /** True once mode is final. False for `'auto'` until the initialize handler resolves client capabilities. */
179
+ /** True once the server mode is final: at construction for explicit `default`/`apps`, or after
180
+ * the initialize handler resolves `'auto'`. Composing before this in `'auto'` mode would use
181
+ * the preliminary DEFAULT mode and produce the wrong (non-widget) tool variants, so composition
182
+ * waits for it. Distinct from {@link clientKnown}, which only withholds client-gated tools. */
160
183
  Object.defineProperty(this, "serverModeResolved", {
161
184
  enumerable: true,
162
185
  configurable: true,
@@ -164,12 +187,13 @@ export class ActorsMcpServer {
164
187
  value: void 0
165
188
  });
166
189
  /**
167
- * Tool requests queued before mode is final. Actor tools are upserted immediately
168
- * (mode-agnostic); we also capture the exact actor-tool slice fetched for each
169
- * request so the flush composes every entry against *its own* actor list rather
170
- * than the accumulated union across unrelated requests.
190
+ * Tool sources queued until composition is possible. Enqueued when the mode is not yet resolved
191
+ * (`'auto'` before initialize), and re-composed by the initialize flush — which is also when the
192
+ * client becomes known, so any client-gated tools withheld by an eager compose are added then.
193
+ * We capture the exact actor-tool slice fetched for each request so the flush composes every
194
+ * entry against *its own* actor list rather than the accumulated union across unrelated requests.
171
195
  */
172
- Object.defineProperty(this, "pendingToolsAfterModeResolved", {
196
+ Object.defineProperty(this, "pendingToolsUntilClientKnown", {
173
197
  enumerable: true,
174
198
  configurable: true,
175
199
  writable: true,
@@ -314,6 +338,8 @@ export class ActorsMcpServer {
314
338
  }
315
339
  this.serverModeResolved = true;
316
340
  }
341
+ // Setting this makes `clientKnown` true, so the queued compose below (and any later
342
+ // load) resolves the tool set for this client and applies the per-client blocklist.
317
343
  this.options.initializeRequestData = request;
318
344
  log.info('Resolved server mode for client capabilities', {
319
345
  serverMode: this.serverMode,
@@ -321,23 +347,74 @@ export class ActorsMcpServer {
321
347
  clientSupportsUi: this.clientSupportsUi,
322
348
  capabilities: (_a = request === null || request === void 0 ? void 0 : request.params) === null || _a === void 0 ? void 0 : _a.capabilities,
323
349
  });
324
- this.updateToolsAfterServerModeResolved();
350
+ this.composePendingToolsForClient();
325
351
  await this.resolveWidgets();
326
352
  const result = await sdkInitHandler(request);
327
- result.instructions = getServerInstructions(this.serverMode);
353
+ // Tools are final here (composePendingToolsForClient ran above, applying the per-client
354
+ // blocklist), so tool presence is the ground truth for whether to advertise
355
+ // report-problem in the instructions.
356
+ result.instructions = getServerInstructions(this.serverMode, this.tools.has(HELPER_TOOLS.PROBLEM_REPORT));
328
357
  return result;
329
358
  });
330
359
  }
331
- updateToolsAfterServerModeResolved() {
332
- if (this.pendingToolsAfterModeResolved.length === 0)
360
+ /** True once the connecting client is known (set in the initialize handler, or hydrated by a
361
+ * recovery path). Only client-gated tools wait for this so the per-client blocklist can be
362
+ * applied; client-agnostic tools compose regardless. */
363
+ get clientKnown() {
364
+ return this.options.initializeRequestData != null;
365
+ }
366
+ /**
367
+ * Compose the tool list for the current connection: resolve mode-specific tools, then drop
368
+ * report-problem unless it is currently servable (see {@link isReportProblemServable}).
369
+ * report-problem is a default-injected tool (via tools_loader) rather than a category member,
370
+ * gated here by servability. Every other tool composes eagerly, so a recovery/rehydration load
371
+ * without an initialize still restores them. report-problem is withheld until the client is known
372
+ * and re-added by the initialize flush. Used by every input-driven load path and the flush.
373
+ * (loadActorsAsTools upserts actor tools directly; actor tools are never gated, so they need no
374
+ * filtering.)
375
+ */
376
+ composeToolsForClient(input, actorTools) {
377
+ const tools = getToolsForServerMode(input, actorTools, this.serverMode);
378
+ if (this.isReportProblemServable())
379
+ return tools;
380
+ return tools.filter((tool) => tool.name !== HELPER_TOOLS.PROBLEM_REPORT);
381
+ }
382
+ /**
383
+ * Whether report-problem may be served on this connection right now:
384
+ * - Its only function is forwarding submissions via telemetry, so it is never servable when
385
+ * telemetry is disabled (it would just fake an acknowledgement into the void).
386
+ * - It cannot be judged until the connecting client is known, so it is withheld until then;
387
+ * the initialize flush re-composes and adds it if the client allows.
388
+ * Every other tool is unconditionally servable, so recovery loads compose them eagerly and they
389
+ * survive a load that never sees an initialize.
390
+ */
391
+ isReportProblemServable() {
392
+ return (!!this.telemetryEnabled &&
393
+ this.clientKnown &&
394
+ !isReportProblemBlockedForClient(this.options.initializeRequestData));
395
+ }
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
+ composePendingToolsForClient() {
410
+ if (this.pendingToolsUntilClientKnown.length === 0)
333
411
  return;
334
- const tools = this.pendingToolsAfterModeResolved.flatMap(({ input, actorTools }) => getToolsForServerMode(input, actorTools, this.serverMode));
335
- this.pendingToolsAfterModeResolved = [];
336
- // Notify after the flush so shared-state handlers (e.g. Redis recovery) see
337
- // the final tool set, including mode-specific helpers added here. Pre-init,
338
- // `loadToolsByName` may have fired `upsertTools(actorTools, true)` with actor
339
- // tools only (helpers still queued), and `loadToolsFromUrl` / `loadToolsFromInput`
340
- // don't notify at all — this call reconciles both paths to the complete set.
412
+ const tools = this.pendingToolsUntilClientKnown.flatMap(({ input, actorTools }) => this.composeToolsForClient(input, actorTools));
413
+ this.pendingToolsUntilClientKnown = [];
414
+ // Notify after the flush so shared-state handlers (e.g. Redis recovery) see the final tool
415
+ // set. Load paths already upserted the client-agnostic tools pre-init; re-upserting is
416
+ // idempotent, and this pass adds the client-gated tools (e.g. report-problem) now that the
417
+ // client is known, reconciling shared state to the complete set.
341
418
  if (tools.length > 0)
342
419
  this.upsertTools(tools, true);
343
420
  }
@@ -408,6 +485,31 @@ export class ActorsMcpServer {
408
485
  listAllToolNames() {
409
486
  return [...this.listInternalToolNames(), ...this.listActorToolNames(), ...this.listActorMcpServerToolIds()];
410
487
  }
488
+ /**
489
+ * Buffer-or-compose gate shared by the actor-tools loaders. If the server mode isn't resolved
490
+ * yet ('auto' before initialize), queue the whole source for `composePendingToolsForClient` and
491
+ * (if non-empty) upsert the mode-agnostic actor tools immediately with the given `shouldNotify`.
492
+ * Once the mode is resolved, compose the client-specific set via `composeToolsForClient` (which
493
+ * withholds report-problem until the client is known) and upsert it; if the client still isn't
494
+ * known, queue the source so the initialize flush re-composes and adds the client-gated tools.
495
+ *
496
+ * Callers pass different `shouldNotify` values: `loadToolsByName` forwards `actorTools.length > 0`
497
+ * (notify only when actor tools were fetched), while `loadToolsFromUrl` and `loadToolsFromInput`
498
+ * pass `false` and defer to the post-initialize reconcile. See `composePendingToolsForClient`.
499
+ */
500
+ registerFetchedActorTools(input, actorTools, shouldNotify) {
501
+ if (!this.serverModeResolved) {
502
+ this.pendingToolsUntilClientKnown.push({ input, actorTools });
503
+ if (actorTools.length > 0)
504
+ this.upsertTools(actorTools, shouldNotify);
505
+ return;
506
+ }
507
+ const tools = this.composeToolsForClient(input, actorTools);
508
+ if (tools.length > 0)
509
+ this.upsertTools(tools, shouldNotify);
510
+ if (!this.clientKnown)
511
+ this.pendingToolsUntilClientKnown.push({ input, actorTools });
512
+ }
411
513
  /**
412
514
  * Loads missing toolNames from a provided list of tool names.
413
515
  * Skips toolNames that are already loaded and loads only the missing ones.
@@ -424,16 +526,7 @@ export class ActorsMcpServer {
424
526
  actorStore: this.actorStore,
425
527
  paymentProvider: this.options.paymentProvider,
426
528
  });
427
- if (!this.serverModeResolved) {
428
- this.pendingToolsAfterModeResolved.push({ input: restoreInput, actorTools });
429
- if (actorTools.length > 0)
430
- this.upsertTools(actorTools, true);
431
- return;
432
- }
433
- const toolsToLoad = getToolsForServerMode(restoreInput, actorTools, this.serverMode);
434
- if (toolsToLoad.length > 0) {
435
- this.upsertTools(toolsToLoad, actorTools.length > 0);
436
- }
529
+ this.registerFetchedActorTools(restoreInput, actorTools, actorTools.length > 0);
437
530
  }
438
531
  /**
439
532
  * Load Actors as tools, upsert successes into the server, and return both the tool
@@ -458,24 +551,14 @@ export class ActorsMcpServer {
458
551
  actorStore: this.actorStore,
459
552
  paymentProvider: this.options.paymentProvider,
460
553
  });
461
- if (!this.serverModeResolved) {
462
- this.pendingToolsAfterModeResolved.push({ input, actorTools });
463
- if (actorTools.length > 0) {
464
- log.debug('Loading actor tools from query parameters before mode resolution');
465
- this.upsertTools(actorTools, false);
466
- }
467
- return;
468
- }
469
- const tools = getToolsForServerMode(input, actorTools, this.serverMode);
470
- if (tools.length > 0) {
471
- log.debug('Loading tools from query parameters');
472
- this.upsertTools(tools, false);
473
- }
554
+ log.debug('Loading tools from query parameters');
555
+ this.registerFetchedActorTools(input, actorTools, false);
474
556
  }
475
557
  /**
476
- * Two-phase: getActors (async, mode-agnostic Apify fetch) then getToolsForServerMode
477
- * (sync, mode-dependent compose). When mode is unresolved, queue actorTools and let
478
- * the initialize handler compose them later.
558
+ * Two-phase: getActors (async, client-agnostic Apify fetch) then composeToolsForClient
559
+ * (sync compose + servability filter). If the mode isn't resolved yet ('auto' before initialize)
560
+ * the whole source is queued for the flush. Otherwise tools compose immediately; client-gated
561
+ * tools are withheld until the client is known, and the source is queued so the flush adds them.
479
562
  *
480
563
  * Don't move the getActors await into the initialize handler — clients time out
481
564
  * waiting for InitializeResult. The queue buffers already-fetched data, not network
@@ -486,15 +569,7 @@ export class ActorsMcpServer {
486
569
  actorStore: this.actorStore,
487
570
  paymentProvider: this.options.paymentProvider,
488
571
  });
489
- if (!this.serverModeResolved) {
490
- this.pendingToolsAfterModeResolved.push({ input, actorTools });
491
- if (actorTools.length > 0)
492
- this.upsertTools(actorTools);
493
- return;
494
- }
495
- const tools = getToolsForServerMode(input, actorTools, this.serverMode);
496
- if (tools.length > 0)
497
- this.upsertTools(tools);
572
+ this.registerFetchedActorTools(input, actorTools, false);
498
573
  }
499
574
  /** Delete tools from the server and notify the handler.
500
575
  */
@@ -518,6 +593,9 @@ export class ActorsMcpServer {
518
593
  * @returns Array of added/updated tool wrappers
519
594
  */
520
595
  upsertTools(tools, shouldNotifyToolsChangedHandler = false) {
596
+ // Client gating (e.g. hiding report-problem from Anthropic surfaces) is applied earlier, in
597
+ // composeToolsForClient — the single compose choke point where the client is known. Do not
598
+ // filter here: this is a low-level commit point reached before the client is known too.
521
599
  for (const tool of tools) {
522
600
  const stored = this.options.paymentProvider ? this.options.paymentProvider.decorateToolSchema(tool) : tool;
523
601
  this.tools.set(stored.name, stored);
@@ -642,6 +720,18 @@ export class ActorsMcpServer {
642
720
  };
643
721
  });
644
722
  }
723
+ /**
724
+ * Fetches a task by ID, softFail-logging and throwing a client-facing McpError if it doesn't exist.
725
+ */
726
+ async getTaskOrThrow(taskId, mcpSessionId, logTag) {
727
+ const task = await this.taskStore.getTask(taskId, mcpSessionId);
728
+ if (!task) {
729
+ // Client error (invalid/unknown taskId) — softFail to avoid polluting error logs.
730
+ log.softFail(`[${logTag}] Task not found`, { taskId, mcpSessionId, statusCode: 404 });
731
+ throw new McpError(ErrorCode.InvalidParams, `Task "${taskId}" not found`);
732
+ }
733
+ return task;
734
+ }
645
735
  /**
646
736
  * Sets up MCP request handlers for long-running tasks.
647
737
  */
@@ -665,12 +755,7 @@ export class ActorsMcpServer {
665
755
  const { taskId } = params;
666
756
  const mcpSessionId = (_a = params._meta) === null || _a === void 0 ? void 0 : _a.mcpSessionId;
667
757
  log.debug('[GetTaskRequestSchema] Getting task status', { taskId, mcpSessionId });
668
- const task = await this.taskStore.getTask(taskId, mcpSessionId);
669
- if (task)
670
- return task;
671
- // Client error (invalid/unknown taskId) — softFail to avoid polluting error logs.
672
- log.softFail('[GetTaskRequestSchema] Task not found', { taskId, mcpSessionId, statusCode: 404 });
673
- throw new McpError(ErrorCode.InvalidParams, `Task "${taskId}" not found`);
758
+ return await this.getTaskOrThrow(taskId, mcpSessionId, 'GetTaskRequestSchema');
674
759
  });
675
760
  // Get task result payload
676
761
  this.server.setRequestHandler(GetTaskPayloadRequestSchema, async (request) => {
@@ -680,12 +765,7 @@ export class ActorsMcpServer {
680
765
  const { taskId } = params;
681
766
  const mcpSessionId = (_a = params._meta) === null || _a === void 0 ? void 0 : _a.mcpSessionId;
682
767
  log.debug('[GetTaskPayloadRequestSchema] Getting task result', { taskId, mcpSessionId });
683
- const task = await this.taskStore.getTask(taskId, mcpSessionId);
684
- if (!task) {
685
- // Client error (invalid/unknown taskId) — softFail to avoid polluting error logs.
686
- log.softFail('[GetTaskPayloadRequestSchema] Task not found', { taskId, mcpSessionId, statusCode: 404 });
687
- throw new McpError(ErrorCode.InvalidParams, `Task "${taskId}" not found`);
688
- }
768
+ const task = await this.getTaskOrThrow(taskId, mcpSessionId, 'GetTaskPayloadRequestSchema');
689
769
  if (task.status !== 'completed' && task.status !== 'failed') {
690
770
  throw new McpError(ErrorCode.InvalidParams, `Task "${taskId}" is not completed yet. Current status: ${task.status}`);
691
771
  }
@@ -707,12 +787,7 @@ export class ActorsMcpServer {
707
787
  const { taskId } = params;
708
788
  const mcpSessionId = (_a = params._meta) === null || _a === void 0 ? void 0 : _a.mcpSessionId;
709
789
  log.debug('[CancelTaskRequestSchema] Cancelling task', { taskId, mcpSessionId });
710
- const task = await this.taskStore.getTask(taskId, mcpSessionId);
711
- if (!task) {
712
- // Client error (invalid/unknown taskId) — softFail to avoid polluting error logs.
713
- log.softFail('[CancelTaskRequestSchema] Task not found', { taskId, mcpSessionId, statusCode: 404 });
714
- throw new McpError(ErrorCode.InvalidParams, `Task "${taskId}" not found`);
715
- }
790
+ const task = await this.getTaskOrThrow(taskId, mcpSessionId, 'CancelTaskRequestSchema');
716
791
  if (task.status === 'completed' || task.status === 'failed' || task.status === 'cancelled') {
717
792
  // Client error (cancel on terminal task) — softFail to avoid polluting error logs.
718
793
  log.softFail('[CancelTaskRequestSchema] Task already in terminal state', {
@@ -772,11 +847,19 @@ export class ActorsMcpServer {
772
847
  let callDiagnostics = {};
773
848
  let shouldTrackTelemetry = true;
774
849
  let resolvedToolName = name;
850
+ // Set only on the pre-flight task path — the one task-mode flow whose telemetry rides
851
+ // this handler's `finally` — so its `Tool call completed` log line keeps the taskId the
852
+ // async path logs via finishTaskTracking.
853
+ let preflightTaskId;
775
854
  // Captured by `captureResult` so the `finally` block can measure response size for telemetry.
776
855
  let toolResult = null;
777
856
  const captureResult = (r) => {
778
- toolResult = r;
779
- return r;
857
+ // On a failed result, nudge the agent to report the blocker via report-problem at the
858
+ // moment it decides what to do next. Gated on the tool actually being served (see
859
+ // 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);
861
+ toolResult = augmented;
862
+ return augmented;
780
863
  };
781
864
  const failInvalidParams = async (message, details, logFields) => {
782
865
  toolStatus = TOOL_STATUS.SOFT_FAIL;
@@ -918,6 +1001,50 @@ export class ActorsMcpServer {
918
1001
  toolName: tool.name,
919
1002
  mcpSessionId,
920
1003
  });
1004
+ // Pre-flight failure is already known — the outcome needs no work. Resolve the
1005
+ // task synchronously: store the failure as the terminal `completed` result and emit
1006
+ // exactly one `completed` status notification (no `updateTaskStatus('working')`, so no
1007
+ // `working` notification). Standby rejection wins over the generic payment-required
1008
+ // short-circuit, matching the sync path's precedence. Telemetry rides the handler's
1009
+ // outer `finally` (shouldTrackTelemetry stays true), firing once with the sync-path
1010
+ // properties plus the taskId on the log line.
1011
+ const preflightResult = standbyRejection !== null && standbyRejection !== void 0 ? standbyRejection : paymentRequiredResult;
1012
+ if (preflightResult) {
1013
+ const outcome = buildPreflightFailureOutcome(standbyRejection, paymentRequiredResult, actorName, actorId);
1014
+ toolStatus = outcome.toolStatus;
1015
+ callDiagnostics = outcome.callDiagnostics;
1016
+ preflightTaskId = task.taskId;
1017
+ try {
1018
+ await storeTaskResultOrSkipIfExpired(this.taskStore, tool.name, task.taskId, 'completed', outcome.result, mcpSessionId);
1019
+ }
1020
+ catch (error) {
1021
+ // A store failure (not expiry) would otherwise fall through to the generic
1022
+ // catch and return a task-less tool result the client rejects as a
1023
+ // CreateTaskResult parse error; surface it as a protocol error instead.
1024
+ // The pre-flight outcome left toolStatus=SOFT_FAIL, but a genuine store
1025
+ // outage is a hard failure — correct it before throwing so the handler
1026
+ // `finally` logs FAILED/INTERNAL_ERROR (the outer McpError re-throw preserves
1027
+ // these), not the stale pre-flight SOFT_FAIL.
1028
+ toolStatus = TOOL_STATUS.FAILED;
1029
+ callDiagnostics = {
1030
+ failure_category: FAILURE_CATEGORY.INTERNAL_ERROR,
1031
+ ...buildActorFields(actorName, actorId),
1032
+ };
1033
+ throw new McpError(ErrorCode.InternalError, `Failed to store the pre-flight result for task "${task.taskId}": ${error instanceof Error ? error.message : String(error)}`);
1034
+ }
1035
+ // Defer so the client sees the CreateTaskResult (and learns the taskId) before
1036
+ // the terminal status notification — the async path's post-response ordering.
1037
+ // emitTaskStatusNotification never throws and no-ops if the task expired.
1038
+ setImmediate(() => {
1039
+ void emitTaskStatusNotification(task.taskId, mcpSessionId, this.taskStore, this.server);
1040
+ });
1041
+ captureResult(outcome.result);
1042
+ // createTask returned status `working`; synthesize the terminal status instead of
1043
+ // re-fetching — if the task expired before the result store (the one case
1044
+ // storeTaskResultOrSkipIfExpired tolerates), a re-fetch would come back empty and a
1045
+ // `working` fallback would contradict the tasks/get 404 the client sees next.
1046
+ return { task: { ...task, status: 'completed' } };
1047
+ }
921
1048
  // Execute the tool asynchronously and update task status
922
1049
  setImmediate(() => {
923
1050
  this.executeToolAndUpdateTask({
@@ -925,8 +1052,6 @@ export class ActorsMcpServer {
925
1052
  tool,
926
1053
  toolArgs: toolArgs,
927
1054
  logSafeArgs,
928
- standbyRejection,
929
- paymentRequiredResult,
930
1055
  apifyClient: apifyClient,
931
1056
  apifyToken,
932
1057
  progressToken,
@@ -943,25 +1068,14 @@ export class ActorsMcpServer {
943
1068
  shouldTrackTelemetry = false;
944
1069
  return { task };
945
1070
  }
946
- // Sync path: standby rejection wins over the generic payment-required short-circuit
947
- // so the agent gets the precise reason instead of a 402.
948
- if (standbyRejection) {
949
- toolStatus = TOOL_STATUS.SOFT_FAIL;
950
- callDiagnostics = {
951
- failure_category: FAILURE_CATEGORY.INVALID_INPUT,
952
- ...buildActorFields(actorName, actorId),
953
- };
954
- return captureResult(standbyRejection);
955
- }
956
- // Check payment validation (already computed by prepareToolCallContext)
957
- if (paymentRequiredResult) {
958
- toolStatus = TOOL_STATUS.SOFT_FAIL;
959
- callDiagnostics = {
960
- failure_category: FAILURE_CATEGORY.INVALID_INPUT,
961
- failure_http_status: 402,
962
- ...buildActorFields(actorName, actorId),
963
- };
964
- return captureResult(paymentRequiredResult);
1071
+ // Sync path: short-circuit on either pre-flight failure. buildPreflightFailureOutcome
1072
+ // encodes the precedence — standby rejection wins over the generic payment-required
1073
+ // 402, so the agent gets the precise reason.
1074
+ if (standbyRejection || paymentRequiredResult) {
1075
+ const outcome = buildPreflightFailureOutcome(standbyRejection, paymentRequiredResult, actorName, actorId);
1076
+ toolStatus = outcome.toolStatus;
1077
+ callDiagnostics = outcome.callDiagnostics;
1078
+ return captureResult(outcome.result);
965
1079
  }
966
1080
  // Handle internal tool
967
1081
  if (tool.type === TOOL_TYPE.INTERNAL) {
@@ -984,9 +1098,7 @@ export class ActorsMcpServer {
984
1098
  mcpSessionId,
985
1099
  }));
986
1100
  // Extract diagnostics and strip internal fields from res before returning to client.
987
- const diag = extractToolTelemetry(res, actorName, actorId);
988
- toolStatus = diag.toolStatus;
989
- callDiagnostics = { ...callDiagnostics, ...diag.callDiagnostics };
1101
+ ({ toolStatus, callDiagnostics } = applyToolTelemetry(res, actorName, actorId, callDiagnostics));
990
1102
  return captureResult(res);
991
1103
  }
992
1104
  finally {
@@ -1097,6 +1209,10 @@ export class ActorsMcpServer {
1097
1209
  // https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/cancellation#behavior-requirements
1098
1210
  return captureResult({});
1099
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));
1100
1216
  return captureResult(executorResult);
1101
1217
  }
1102
1218
  finally {
@@ -1186,6 +1302,7 @@ export class ActorsMcpServer {
1186
1302
  callDiagnostics,
1187
1303
  args,
1188
1304
  result: toolResult,
1305
+ taskId: preflightTaskId,
1189
1306
  });
1190
1307
  }
1191
1308
  }
@@ -1243,6 +1360,13 @@ export class ActorsMcpServer {
1243
1360
  ...deriveResourceIds(params.args, params.result),
1244
1361
  };
1245
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
+ }
1246
1370
  }
1247
1371
  }
1248
1372
  // TODO: this function quite duplicates the main tool call login the CallToolRequestSchema handler, we should refactor
@@ -1259,7 +1383,7 @@ export class ActorsMcpServer {
1259
1383
  * @param params.mcpSessionId - MCP session ID for telemetry
1260
1384
  */
1261
1385
  async executeToolAndUpdateTask(params) {
1262
- const { taskId, tool, toolArgs, logSafeArgs, standbyRejection, paymentRequiredResult, apifyClient, apifyToken, progressToken, extra, mcpSessionId, actorName, actorId, } = params;
1386
+ const { taskId, tool, toolArgs, logSafeArgs, apifyClient, apifyToken, progressToken, extra, mcpSessionId, actorName, actorId, } = params;
1263
1387
  let toolStatus = TOOL_STATUS.SUCCEEDED;
1264
1388
  // Always populate actor fields so they're tracked on both success and failure paths.
1265
1389
  let callDiagnostics = { ...buildActorFields(actorName, actorId) };
@@ -1331,25 +1455,6 @@ export class ActorsMcpServer {
1331
1455
  await emitTaskStatusNotification(taskId, mcpSessionId, this.taskStore, this.server);
1332
1456
  // Execute the tool and get the result
1333
1457
  let result = {};
1334
- // Mirror the sync-path ordering: standby rejection wins over the generic 402 so the
1335
- // stored task result carries the precise reason the agent should act on.
1336
- if (standbyRejection) {
1337
- toolStatus = TOOL_STATUS.SOFT_FAIL;
1338
- callDiagnostics = {
1339
- failure_category: FAILURE_CATEGORY.INVALID_INPUT,
1340
- ...buildActorFields(actorName, actorId),
1341
- };
1342
- result = standbyRejection;
1343
- }
1344
- else if (paymentRequiredResult) {
1345
- toolStatus = TOOL_STATUS.SOFT_FAIL;
1346
- callDiagnostics = {
1347
- failure_category: FAILURE_CATEGORY.INVALID_INPUT,
1348
- failure_http_status: 402,
1349
- ...buildActorFields(actorName, actorId),
1350
- };
1351
- result = paymentRequiredResult;
1352
- }
1353
1458
  // Callback to propagate Actor run statusMessage into the task store and emit a push notification.
1354
1459
  // TODO(TC-3): cancel arriving while this is scheduled throws cancelled → working;
1355
1460
  // currently swallowed by progress.ts's tick catch — guard or catch explicitly.
@@ -1378,9 +1483,7 @@ export class ActorsMcpServer {
1378
1483
  mcpSessionId,
1379
1484
  taskMode: true,
1380
1485
  }));
1381
- const diag = extractToolTelemetry(res, actorName, actorId);
1382
- toolStatus = diag.toolStatus;
1383
- callDiagnostics = { ...callDiagnostics, ...diag.callDiagnostics };
1486
+ ({ toolStatus, callDiagnostics } = applyToolTelemetry(res, actorName, actorId, callDiagnostics));
1384
1487
  result = res;
1385
1488
  }
1386
1489
  finally {
@@ -1418,6 +1521,9 @@ export class ActorsMcpServer {
1418
1521
  result = {};
1419
1522
  }
1420
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));
1421
1527
  result = executorResult;
1422
1528
  }
1423
1529
  }
@@ -1430,6 +1536,9 @@ export class ActorsMcpServer {
1430
1536
  // Check if task was cancelled before storing result
1431
1537
  if (await skipIfTaskCancelled(', skipping result storage', toolStatus))
1432
1538
  return;
1539
+ // On a failed result, nudge the agent to report the blocker via report-problem (mirrors the
1540
+ // 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);
1433
1542
  // Store the result in the task store
1434
1543
  log.debug('[executeToolAndUpdateTask] Storing completed result', {
1435
1544
  taskId,
@@ -1542,7 +1651,11 @@ export class ActorsMcpServer {
1542
1651
  taskId,
1543
1652
  mcpSessionId,
1544
1653
  });
1545
- const failedResult = {
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({
1546
1659
  content: [
1547
1660
  {
1548
1661
  type: 'text',
@@ -1551,7 +1664,7 @@ export class ActorsMcpServer {
1551
1664
  ],
1552
1665
  isError: true,
1553
1666
  internalToolStatus: toolStatus,
1554
- };
1667
+ }, tool.name, callDiagnostics.failure_category, callDiagnostics.failure_http_status);
1555
1668
  await storeTaskResultOrSkipIfExpired(this.taskStore, tool.name, taskId, 'failed', failedResult, mcpSessionId);
1556
1669
  await emitTaskStatusNotification(taskId, mcpSessionId, this.taskStore, this.server);
1557
1670
  finishTaskTracking(toolStatus, callDiagnostics, failedResult);