@apify/actors-mcp-server 0.9.17-beta.2 → 0.9.17-beta.3

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 (67) hide show
  1. package/dist/const.d.ts +9 -1
  2. package/dist/const.d.ts.map +1 -1
  3. package/dist/const.js +9 -1
  4. package/dist/const.js.map +1 -1
  5. package/dist/mcp/server.d.ts +1 -0
  6. package/dist/mcp/server.d.ts.map +1 -1
  7. package/dist/mcp/server.js +298 -180
  8. package/dist/mcp/server.js.map +1 -1
  9. package/dist/payments/helpers.d.ts +18 -13
  10. package/dist/payments/helpers.d.ts.map +1 -1
  11. package/dist/payments/helpers.js +21 -16
  12. package/dist/payments/helpers.js.map +1 -1
  13. package/dist/payments/index.d.ts +2 -2
  14. package/dist/payments/index.d.ts.map +1 -1
  15. package/dist/payments/index.js +1 -1
  16. package/dist/payments/index.js.map +1 -1
  17. package/dist/tools/common/fetch_apify_docs.d.ts.map +1 -1
  18. package/dist/tools/common/fetch_apify_docs.js +9 -4
  19. package/dist/tools/common/fetch_apify_docs.js.map +1 -1
  20. package/dist/tools/common/get_actor_output.js +2 -2
  21. package/dist/tools/common/get_actor_output.js.map +1 -1
  22. package/dist/tools/common/get_dataset.js +2 -2
  23. package/dist/tools/common/get_dataset.js.map +1 -1
  24. package/dist/tools/common/get_dataset_items.js +1 -1
  25. package/dist/tools/common/get_dataset_items.js.map +1 -1
  26. package/dist/tools/common/get_dataset_schema.js +2 -2
  27. package/dist/tools/common/get_dataset_schema.js.map +1 -1
  28. package/dist/tools/core/actor_tools_factory.d.ts +1 -0
  29. package/dist/tools/core/actor_tools_factory.d.ts.map +1 -1
  30. package/dist/tools/core/actor_tools_factory.js +5 -1
  31. package/dist/tools/core/actor_tools_factory.js.map +1 -1
  32. package/dist/tools/core/call_actor_common.d.ts +1 -11
  33. package/dist/tools/core/call_actor_common.d.ts.map +1 -1
  34. package/dist/tools/core/call_actor_common.js +63 -18
  35. package/dist/tools/core/call_actor_common.js.map +1 -1
  36. package/dist/tools/core/get_actor_run_common.js +2 -2
  37. package/dist/tools/core/get_actor_run_common.js.map +1 -1
  38. package/dist/tools/default/call_actor.d.ts.map +1 -1
  39. package/dist/tools/default/call_actor.js +22 -4
  40. package/dist/tools/default/call_actor.js.map +1 -1
  41. package/dist/tools/default/get_actor_run.js +1 -1
  42. package/dist/tools/default/get_actor_run.js.map +1 -1
  43. package/dist/tools/openai/call_actor.d.ts.map +1 -1
  44. package/dist/tools/openai/call_actor.js +21 -4
  45. package/dist/tools/openai/call_actor.js.map +1 -1
  46. package/dist/tools/openai/get_actor_run.js +1 -1
  47. package/dist/tools/openai/get_actor_run.js.map +1 -1
  48. package/dist/tsconfig.tsbuildinfo +1 -1
  49. package/dist/types.d.ts +28 -1
  50. package/dist/types.d.ts.map +1 -1
  51. package/dist/types.js.map +1 -1
  52. package/dist/utils/actor_details.js +2 -2
  53. package/dist/utils/actor_details.js.map +1 -1
  54. package/dist/utils/mcp.d.ts +9 -18
  55. package/dist/utils/mcp.d.ts.map +1 -1
  56. package/dist/utils/mcp.js +8 -17
  57. package/dist/utils/mcp.js.map +1 -1
  58. package/dist/utils/payment_errors.d.ts +1 -1
  59. package/dist/utils/tool_status.d.ts +20 -2
  60. package/dist/utils/tool_status.d.ts.map +1 -1
  61. package/dist/utils/tool_status.js +90 -2
  62. package/dist/utils/tool_status.js.map +1 -1
  63. package/dist/utils/tools.d.ts +22 -1
  64. package/dist/utils/tools.d.ts.map +1 -1
  65. package/dist/utils/tools.js +50 -0
  66. package/dist/utils/tools.js.map +1 -1
  67. package/package.json +1 -1
@@ -5,11 +5,12 @@ import { randomUUID } from 'node:crypto';
5
5
  import { InMemoryTaskStore } from '@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js';
6
6
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
7
7
  import { CallToolRequestSchema, CallToolResultSchema, CancelTaskRequestSchema, ErrorCode, GetPromptRequestSchema, GetTaskPayloadRequestSchema, GetTaskRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListTasksRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema, ServerNotificationSchema, SetLevelRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
8
+ import dedent from 'dedent';
8
9
  import log from '@apify/log';
9
10
  import { parseBooleanOrNull } from '@apify/utilities';
10
11
  import { ApifyClient } from '../apify_client.js';
11
- import { ALLOWED_TASK_TOOL_EXECUTION_MODES, APIFY_MCP_URL, DEFAULT_TELEMETRY_ENABLED, DEFAULT_TELEMETRY_ENV, HelperTools, HTTP_PAYMENT_REQUIRED, SERVER_NAME, TOOL_STATUS, } from '../const.js';
12
- import { preparePayment } from '../payments/helpers.js';
12
+ import { ALLOWED_TASK_TOOL_EXECUTION_MODES, APIFY_MCP_URL, DEFAULT_TELEMETRY_ENABLED, DEFAULT_TELEMETRY_ENV, FAILURE_CATEGORY, HelperTools, HTTP_PAYMENT_REQUIRED, SERVER_NAME, TOOL_STATUS, } from '../const.js';
13
+ import { prepareToolCallContext } from '../payments/helpers.js';
13
14
  import { prompts } from '../prompts/index.js';
14
15
  import { createResourceService } from '../resources/resource_service.js';
15
16
  import { resolveAvailableWidgets, RESOURCE_MIME_TYPE } from '../resources/widgets.js';
@@ -23,8 +24,8 @@ import { buildMCPResponse } from '../utils/mcp.js';
23
24
  import { buildPaymentRequiredResponse } from '../utils/payment_errors.js';
24
25
  import { createProgressTracker } from '../utils/progress.js';
25
26
  import { getServerInstructions } from '../utils/server-instructions/index.js';
26
- import { getToolStatusFromError } from '../utils/tool_status.js';
27
- import { getToolPublicFieldOnly } from '../utils/tools.js';
27
+ import { classifyFailureCategory, extractAjvErrorDetails, extractToolTelemetry, getToolStatusFromError } from '../utils/tool_status.js';
28
+ import { buildActorFields, extractActorId, extractActorName, getToolFullName, getToolPublicFieldOnly } from '../utils/tools.js';
28
29
  import { getUserIdFromTokenCached } from '../utils/userid_cache.js';
29
30
  import { getPackageVersion } from '../utils/version.js';
30
31
  import { connectMCPClient } from './client.js';
@@ -591,7 +592,7 @@ export class ActorsMcpServer {
591
592
  * @throws {McpError} - based on the McpServer class code from the typescript MCP SDK
592
593
  */
593
594
  this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
594
- var _a, _b, _c, _d, _e;
595
+ var _a, _b, _c, _d, _e, _f;
595
596
  const params = request.params;
596
597
  // eslint-disable-next-line prefer-const
597
598
  let { name, arguments: args, _meta: meta } = params;
@@ -605,104 +606,156 @@ export class ActorsMcpServer {
605
606
  log.error('MCP Session ID is missing in tool call request. This should never happen.');
606
607
  throw new Error('MCP Session ID is required for tool calls');
607
608
  }
608
- // Validate token
609
- if (!apifyToken && !((_a = this.options.paymentProvider) === null || _a === void 0 ? void 0 : _a.allowsUnauthenticated) && !this.options.allowUnauthMode) {
610
- const msg = `Apify API token is required but was not provided.
611
- Please set the APIFY_TOKEN environment variable or pass it as a parameter in the request header as Authorization Bearer <token>.
612
- You can obtain your Apify token from https://console.apify.com/account/integrations.`;
613
- log.softFail(msg, { mcpSessionId, statusCode: 400 });
614
- await this.server.sendLoggingMessage({ level: 'error', data: msg });
615
- throw new McpError(ErrorCode.InvalidParams, msg);
616
- }
617
- // TODO - if connection is /mcp client will not receive notification on tool change
618
- // Find tool by name, actor full name, or legacy tool name (e.g. apify-slash-rag-web-browser → apify--rag-web-browser)
619
- const newName = (_b = legacyToolNameToNew(name)) !== null && _b !== void 0 ? _b : name;
620
- const tool = Array.from(this.tools.values())
621
- .find((t) => t.name === newName || (t.type === 'actor' && t.actorFullName === newName));
622
- if (!tool) {
623
- const availableTools = this.listToolNames();
624
- const msg = `Tool "${name}" was not found.
625
- Available tools: ${availableTools.length > 0 ? availableTools.join(', ') : 'none'}.
626
- Please verify the tool name is correct. You can list all available tools using the tools/list request.`;
627
- log.softFail(msg, { mcpSessionId, statusCode: 404 });
628
- await this.server.sendLoggingMessage({ level: 'error', data: msg });
629
- throw new McpError(ErrorCode.InvalidParams, msg);
630
- }
631
- if (!args) {
632
- const msg = `Missing arguments for tool "${name}".
633
- Please provide the required arguments for this tool. Check the tool's input schema using ${HelperTools.ACTOR_GET_DETAILS} tool to see what parameters are required.`;
634
- log.softFail(msg, { mcpSessionId, statusCode: 400 });
635
- await this.server.sendLoggingMessage({ level: 'error', data: msg });
636
- throw new McpError(ErrorCode.InvalidParams, msg);
637
- }
638
- // Decode dot property names in arguments before validation,
639
- // since validation expects the original, non-encoded property names.
640
- args = decodeDotPropertyNames(args);
641
- // Centralize all payment processing: validate, strip payment fields, create client.
642
- // Must run before ajv validation so cleanArgs doesn't contain provider-specific fields.
643
- const payment = preparePayment({
644
- provider: this.options.paymentProvider,
645
- tool,
646
- args: args,
647
- apifyToken,
648
- meta,
649
- requestHeaders: (_c = extra.requestInfo) === null || _c === void 0 ? void 0 : _c.headers,
650
- });
651
- log.debug('Validate arguments for tool', { toolName: tool.name, mcpSessionId, input: payment.logArgs });
652
- if (!tool.ajvValidate(payment.cleanArgs)) {
653
- const errors = (tool === null || tool === void 0 ? void 0 : tool.ajvValidate.errors) || [];
654
- const errorMessages = errors.map((e) => `${e.instancePath || 'root'}: ${e.message || 'validation error'}`).join('; ');
655
- const msg = `Invalid arguments for tool "${tool.name}".
656
- Validation errors: ${errorMessages}.
657
- Please check the tool's input schema using ${HelperTools.ACTOR_GET_DETAILS} tool and ensure all required parameters are provided with correct types and values.`;
658
- log.softFail(msg, { mcpSessionId, statusCode: 400 });
659
- await this.server.sendLoggingMessage({ level: 'error', data: msg });
660
- throw new McpError(ErrorCode.InvalidParams, msg);
661
- }
662
- // TODO: we should split this huge method into smaller parts as it is slowly getting out of hand
663
- // Check if tool call is a long running task and the tool supports that
664
- // Cast to allowed task mode types ('optional' | 'required') for type-safe includes() check
665
- const taskSupport = (_d = tool.execution) === null || _d === void 0 ? void 0 : _d.taskSupport;
666
- if (request.params.task && !ALLOWED_TASK_TOOL_EXECUTION_MODES.includes(taskSupport)) {
667
- const msg = `Tool "${tool.name}" does not support long running task calls.
668
- Please remove the "task" parameter from the tool call request or use a different tool that supports long running tasks.`;
669
- log.softFail(msg, { mcpSessionId, statusCode: 400 });
670
- await this.server.sendLoggingMessage({ level: 'error', data: msg });
671
- throw new McpError(ErrorCode.InvalidParams, msg);
672
- }
673
- // Handle long-running task request
674
- if (request.params.task) {
675
- const task = await this.taskStore.createTask({
676
- ttl: request.params.task.ttl,
677
- }, `call-tool-${name}-${randomUUID()}`, request);
678
- log.debug('Created task for tool execution', { taskId: task.taskId, toolName: tool.name, mcpSessionId });
679
- // Execute the tool asynchronously and update task status
680
- setImmediate(async () => {
681
- await this.executeToolAndUpdateTask({
682
- taskId: task.taskId,
683
- tool,
684
- cleanArgs: payment.cleanArgs,
685
- logArgs: payment.logArgs,
686
- paymentErrorResult: payment.errorResult,
687
- apifyClient: payment.client,
688
- apifyToken,
689
- progressToken,
690
- extra,
691
- mcpSessionId,
692
- userRentedActorIds,
693
- });
694
- });
695
- // Return the task immediately; execution continues asynchronously
696
- return { task };
697
- }
698
- const { telemetryData, userId } = await this.prepareTelemetryData(tool, mcpSessionId, apifyToken);
699
609
  const startTime = Date.now();
610
+ let telemetryData;
611
+ let userId;
700
612
  let toolStatus = TOOL_STATUS.SUCCEEDED;
613
+ let callDiagnostics = {};
614
+ let shouldTrackTelemetry = true;
615
+ const failInvalidParams = async (message, details, logFields) => {
616
+ toolStatus = TOOL_STATUS.SOFT_FAIL;
617
+ callDiagnostics = details;
618
+ log.softFail(message, {
619
+ mcpSessionId,
620
+ failureCategory: details.failure_category,
621
+ actorName: details.actor_name,
622
+ validationKeyword: details.validation_keyword,
623
+ validationPath: details.validation_path,
624
+ validationMissingProperty: details.validation_missing_property,
625
+ validationAdditionalProperty: details.validation_additional_property,
626
+ ...logFields,
627
+ });
628
+ await this.server.sendLoggingMessage({ level: 'error', data: message });
629
+ throw new McpError(ErrorCode.InvalidParams, message);
630
+ };
631
+ // Initialize telemetry with raw tool name — may be overwritten below once the tool is resolved.
632
+ // This ensures telemetry is available even for early failures (missing token, tool not found).
633
+ ({ telemetryData, userId } = await this.prepareTelemetryData(name, mcpSessionId, apifyToken));
634
+ // actorName/actorId are declared here so they're available in the catch block for telemetry.
635
+ // Set after tool resolution (inside the try block).
636
+ let actorName;
637
+ let actorId;
701
638
  try {
702
- // Check payment validation (already computed by preparePayment)
703
- if (payment.errorResult) {
639
+ // Validate token
640
+ if (!apifyToken && !((_a = this.options.paymentProvider) === null || _a === void 0 ? void 0 : _a.allowsUnauthenticated) && !this.options.allowUnauthMode) {
641
+ await failInvalidParams(dedent `
642
+ Apify API token is required but was not provided.
643
+ Please set the APIFY_TOKEN environment variable or pass it as a parameter in the request header as Authorization Bearer <token>.
644
+ You can get your Apify token from https://console.apify.com/account/integrations.
645
+ `, {
646
+ failure_category: FAILURE_CATEGORY.AUTH,
647
+ });
648
+ }
649
+ // TODO - if connection is /mcp client will not receive notification on tool change
650
+ // Find tool by name, actor full name, or legacy tool name (e.g. apify-slash-rag-web-browser → apify--rag-web-browser)
651
+ const newName = (_b = legacyToolNameToNew(name)) !== null && _b !== void 0 ? _b : name;
652
+ const toolEntry = Array.from(this.tools.values())
653
+ .find((t) => t.name === newName || getToolFullName(t) === newName);
654
+ if (!toolEntry) {
655
+ const availableTools = this.listToolNames();
656
+ await failInvalidParams(dedent `
657
+ Tool "${name}" was not found.
658
+ Available tools: ${availableTools.length > 0 ? availableTools.join(', ') : 'none'}.
659
+ Please verify the tool name is correct. You can list all available tools using the tools/list request.
660
+ `, {
661
+ failure_category: FAILURE_CATEGORY.INVALID_INPUT,
662
+ });
663
+ }
664
+ const tool = toolEntry;
665
+ // Re-initialize telemetry with the resolved tool (uses actorFullName for actor tools).
666
+ ({ telemetryData, userId } = await this.prepareTelemetryData(getToolFullName(tool), mcpSessionId, apifyToken));
667
+ // Extract actor name/id for telemetry — available even when validation fails later.
668
+ actorName = extractActorName(tool, args);
669
+ actorId = extractActorId(tool);
670
+ // Always populate actor fields so they're tracked on both success and failure paths.
671
+ callDiagnostics = { ...callDiagnostics, ...buildActorFields(actorName, actorId) };
672
+ if (!args) {
673
+ await failInvalidParams(dedent `
674
+ Missing arguments for tool "${name}".
675
+ Please provide the required arguments for this tool. Check the tool's input schema using ${HelperTools.ACTOR_GET_DETAILS} tool to see what parameters are required.
676
+ `, {
677
+ failure_category: FAILURE_CATEGORY.INVALID_INPUT,
678
+ ...buildActorFields(actorName, actorId),
679
+ });
680
+ }
681
+ // Decode dot property names in arguments before validation,
682
+ // since validation expects the original, non-encoded property names.
683
+ args = decodeDotPropertyNames(args);
684
+ // Centralize all payment processing: validate, strip payment fields, create client.
685
+ // Must run before ajv validation so toolArgs doesn't contain provider-specific fields.
686
+ const { toolArgs, logSafeArgs, apifyClient, paymentRequiredResult } = prepareToolCallContext({
687
+ provider: this.options.paymentProvider,
688
+ tool,
689
+ args: args,
690
+ apifyToken,
691
+ meta,
692
+ requestHeaders: (_c = extra.requestInfo) === null || _c === void 0 ? void 0 : _c.headers,
693
+ });
694
+ log.debug('Validate arguments for tool', { toolName: tool.name, mcpSessionId, input: logSafeArgs });
695
+ if (!tool.ajvValidate(toolArgs)) {
696
+ const errors = tool.ajvValidate.errors || [];
697
+ const ajvErrorDetails = extractAjvErrorDetails(errors);
698
+ const errorMessages = errors.map((e) => `${e.instancePath || 'root'}: ${e.message || 'validation error'}`).join('; ');
699
+ await failInvalidParams(dedent `
700
+ Invalid arguments for tool "${tool.name}".
701
+ Validation errors: ${errorMessages}.
702
+ Please check the tool's input schema using ${HelperTools.ACTOR_GET_DETAILS} tool and ensure all required parameters are provided with correct types and values.
703
+ `, {
704
+ failure_category: FAILURE_CATEGORY.INVALID_INPUT,
705
+ ...ajvErrorDetails,
706
+ ...buildActorFields(actorName, actorId),
707
+ });
708
+ }
709
+ // Check if tool call is a long running task and the tool supports that
710
+ // Cast to allowed task mode types ('optional' | 'required') for type-safe includes() check
711
+ const taskSupport = (_d = tool.execution) === null || _d === void 0 ? void 0 : _d.taskSupport;
712
+ if (request.params.task && !ALLOWED_TASK_TOOL_EXECUTION_MODES.includes(taskSupport)) {
713
+ await failInvalidParams(dedent `
714
+ Tool "${tool.name}" does not support long running task calls.
715
+ Please remove the "task" parameter from the tool call request or use a different tool that supports long running tasks.
716
+ `, {
717
+ failure_category: FAILURE_CATEGORY.INVALID_INPUT,
718
+ ...buildActorFields(actorName, actorId),
719
+ });
720
+ }
721
+ // TODO: we should split this huge method into smaller parts as it is slowly getting out of hand
722
+ // Handle long-running task request
723
+ if (request.params.task) {
724
+ const task = await this.taskStore.createTask({
725
+ ttl: request.params.task.ttl,
726
+ }, `call-tool-${name}-${randomUUID()}`, request);
727
+ log.debug('Created task for tool execution', { taskId: task.taskId, toolName: tool.name, mcpSessionId });
728
+ // Execute the tool asynchronously and update task status
729
+ setImmediate(async () => {
730
+ await this.executeToolAndUpdateTask({
731
+ taskId: task.taskId,
732
+ tool,
733
+ toolArgs: toolArgs,
734
+ logSafeArgs,
735
+ paymentRequiredResult,
736
+ apifyClient: apifyClient,
737
+ apifyToken,
738
+ progressToken,
739
+ extra,
740
+ mcpSessionId,
741
+ actorName,
742
+ actorId,
743
+ userRentedActorIds,
744
+ });
745
+ });
746
+ // Return the task immediately; execution continues asynchronously
747
+ shouldTrackTelemetry = false;
748
+ return { task };
749
+ }
750
+ // Check payment validation (already computed by prepareToolCallContext)
751
+ if (paymentRequiredResult) {
704
752
  toolStatus = TOOL_STATUS.SOFT_FAIL;
705
- return payment.errorResult;
753
+ callDiagnostics = {
754
+ failure_category: FAILURE_CATEGORY.INVALID_INPUT,
755
+ failure_http_status: 402,
756
+ ...buildActorFields(actorName, actorId),
757
+ };
758
+ return paymentRequiredResult;
706
759
  }
707
760
  // Handle internal tool
708
761
  if (tool.type === 'internal') {
@@ -710,45 +763,42 @@ Please remove the "task" parameter from the tool call request or use a different
710
763
  const progressTracker = tool.name === 'call-actor'
711
764
  ? createProgressTracker(progressToken, extra.sendNotification)
712
765
  : null;
713
- log.info('Calling internal tool', { name: tool.name, mcpSessionId, input: payment.logArgs });
714
- const res = await tool.call({
715
- args: payment.cleanArgs,
716
- extra,
717
- apifyMcpServer: this,
718
- mcpServer: this.server,
719
- apifyToken,
720
- apifyClient: payment.client,
721
- userRentedActorIds,
722
- progressTracker,
723
- mcpSessionId,
724
- });
725
- if (progressTracker) {
726
- progressTracker.stop();
727
- }
728
- // If tool returned internalToolStatus, use it; otherwise infer from isError flag
729
- const { internalToolStatus, ...rest } = res;
730
- if (internalToolStatus !== undefined) {
731
- toolStatus = internalToolStatus;
732
- }
733
- else if ('isError' in rest && rest.isError) {
734
- toolStatus = TOOL_STATUS.FAILED;
766
+ try {
767
+ log.info('Calling internal tool', { name: tool.name, mcpSessionId, input: logSafeArgs });
768
+ const res = await tool.call({
769
+ args: toolArgs,
770
+ extra,
771
+ apifyMcpServer: this,
772
+ mcpServer: this.server,
773
+ apifyToken,
774
+ apifyClient: apifyClient,
775
+ userRentedActorIds,
776
+ progressTracker,
777
+ mcpSessionId,
778
+ });
779
+ // Extract diagnostics and strip internal fields from res before returning to client.
780
+ const diag = extractToolTelemetry(res, actorName, actorId);
781
+ toolStatus = diag.toolStatus;
782
+ callDiagnostics = { ...callDiagnostics, ...diag.callDiagnostics };
783
+ return res;
735
784
  }
736
- else {
737
- toolStatus = TOOL_STATUS.SUCCEEDED;
785
+ finally {
786
+ progressTracker === null || progressTracker === void 0 ? void 0 : progressTracker.stop();
738
787
  }
739
- // Never expose internalToolStatus to MCP clients
740
- return { ...rest };
741
788
  }
742
789
  if (tool.type === 'actor-mcp') {
743
790
  let client = null;
744
791
  try {
745
792
  client = await connectMCPClient(tool.serverUrl, apifyToken, mcpSessionId);
746
793
  if (!client) {
747
- const msg = `Failed to connect to MCP server at "${tool.serverUrl}".
748
- Please verify the server URL is correct and accessible, and ensure you have a valid Apify token with appropriate permissions.`;
749
- log.softFail(msg, { mcpSessionId, statusCode: 408 }); // 408 Request Timeout
794
+ const msg = dedent `
795
+ Failed to connect to MCP server at "${tool.serverUrl}".
796
+ Please verify the server URL is correct and accessible, and ensure you have a valid Apify token with appropriate permissions.
797
+ `;
798
+ log.softFail(msg, { mcpSessionId, failureCategory: FAILURE_CATEGORY.INTERNAL_ERROR });
750
799
  await this.server.sendLoggingMessage({ level: 'error', data: msg });
751
800
  toolStatus = TOOL_STATUS.SOFT_FAIL;
801
+ callDiagnostics = { ...callDiagnostics, failure_category: FAILURE_CATEGORY.INTERNAL_ERROR };
752
802
  return buildMCPResponse({ texts: [msg], isError: true });
753
803
  }
754
804
  // Only set up notification handlers if progressToken is provided by the client
@@ -771,27 +821,38 @@ Please verify the server URL is correct and accessible, and ensure you have a va
771
821
  actorId: tool.actorId,
772
822
  toolName: tool.originToolName,
773
823
  mcpSessionId,
774
- input: payment.logArgs,
824
+ input: logSafeArgs,
775
825
  });
776
826
  const res = await client.callTool({
777
827
  name: tool.originToolName,
778
- arguments: payment.cleanArgs,
779
- _meta: {
780
- progressToken,
781
- },
828
+ arguments: toolArgs,
829
+ _meta: { progressToken },
782
830
  }, CallToolResultSchema, {
783
831
  timeout: EXTERNAL_TOOL_CALL_TIMEOUT_MSEC,
784
832
  });
785
- // For external MCP servers we do not try to infer soft_fail vs failed from isError.
786
- // We treat the call as succeeded at the telemetry layer unless an actual error is thrown.
833
+ // TODO: actor-mcp responses are opaque isError could be a user input problem
834
+ // (e.g. invalid query) or a genuine server failure. We can't distinguish without
835
+ // parsing the error text. Defaulting to INTERNAL_ERROR for now; revisit when
836
+ // actor-mcp gets deeper telemetry treatment.
837
+ if ('isError' in res && res.isError) {
838
+ toolStatus = TOOL_STATUS.SOFT_FAIL;
839
+ callDiagnostics = { failure_category: FAILURE_CATEGORY.INTERNAL_ERROR, ...buildActorFields(actorName, actorId) };
840
+ }
787
841
  return { ...res };
788
842
  }
789
843
  catch (error) {
844
+ toolStatus = getToolStatusFromError(error, Boolean((_e = extra.signal) === null || _e === void 0 ? void 0 : _e.aborted));
845
+ const failureDetail = error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200);
846
+ callDiagnostics = {
847
+ failure_category: classifyFailureCategory(error),
848
+ failure_detail: failureDetail,
849
+ ...buildActorFields(actorName, actorId),
850
+ };
790
851
  logHttpError(error, `Failed to call MCP tool '${tool.originToolName}' on Actor '${tool.actorId}'`, {
791
852
  actorId: tool.actorId,
792
853
  toolName: tool.originToolName,
854
+ failureCategory: callDiagnostics.failure_category,
793
855
  });
794
- toolStatus = TOOL_STATUS.FAILED;
795
856
  return buildMCPResponse({
796
857
  texts: [`Failed to call MCP tool '${tool.originToolName}' on Actor '${tool.actorId}': ${error instanceof Error ? error.message : String(error)}. The MCP server may be temporarily unavailable.`],
797
858
  isError: true,
@@ -806,11 +867,11 @@ Please verify the server URL is correct and accessible, and ensure you have a va
806
867
  if (tool.type === 'actor') {
807
868
  const progressTracker = createProgressTracker(progressToken, extra.sendNotification);
808
869
  try {
809
- log.info('Calling Actor', { actorName: tool.actorFullName, mcpSessionId, input: payment.logArgs });
870
+ log.info('Calling Actor', { actorName: tool.actorFullName, mcpSessionId, input: logSafeArgs });
810
871
  const executorResult = await this.actorExecutor.executeActorTool({
811
872
  actorFullName: tool.actorFullName,
812
- input: payment.cleanArgs,
813
- apifyClient: payment.client,
873
+ input: toolArgs,
874
+ apifyClient: apifyClient,
814
875
  callOptions: { memory: tool.memoryMbytes },
815
876
  progressTracker,
816
877
  abortSignal: extra.signal,
@@ -834,30 +895,65 @@ Please verify the server URL is correct and accessible, and ensure you have a va
834
895
  toolStatus = TOOL_STATUS.SOFT_FAIL;
835
896
  }
836
897
  catch (error) {
898
+ const httpStatus = getHttpStatusCode(error);
837
899
  // Propagate 402 Payment Required as a tool result per x402 MCP transport spec:
838
900
  // content[0].text (JSON) + isError: true
839
- const httpStatus = getHttpStatusCode(error);
840
901
  if (httpStatus === HTTP_PAYMENT_REQUIRED) {
841
- logHttpError(error, 'Payment required while calling tool', { toolName: name });
842
902
  toolStatus = TOOL_STATUS.SOFT_FAIL;
903
+ callDiagnostics = {
904
+ failure_category: FAILURE_CATEGORY.INVALID_INPUT,
905
+ failure_http_status: 402,
906
+ ...buildActorFields(actorName, actorId),
907
+ };
843
908
  return buildPaymentRequiredResponse(error);
844
909
  }
845
- toolStatus = getToolStatusFromError(error, Boolean((_e = extra.signal) === null || _e === void 0 ? void 0 : _e.aborted));
846
- logHttpError(error, 'Error occurred while calling tool', { toolName: name });
910
+ // Re-throw MCP protocol errors (e.g. from failInvalidParams) so the SDK
911
+ // returns them as JSON-RPC errors. failInvalidParams already set callDiagnostics
912
+ // with the correct semantic category (e.g. AUTH), so we must not overwrite it.
913
+ if (error instanceof McpError) {
914
+ throw error;
915
+ }
916
+ toolStatus = getToolStatusFromError(error, Boolean((_f = extra.signal) === null || _f === void 0 ? void 0 : _f.aborted));
917
+ const failureDetail = error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200);
918
+ callDiagnostics = {
919
+ // Spread existing diagnostics first (e.g. validation_keyword from failInvalidParams),
920
+ // then overwrite with freshly computed fields so they take precedence.
921
+ ...callDiagnostics,
922
+ failure_category: classifyFailureCategory(error),
923
+ ...(httpStatus !== undefined ? { failure_http_status: httpStatus } : {}),
924
+ failure_detail: failureDetail,
925
+ ...buildActorFields(actorName, actorId),
926
+ };
927
+ logHttpError(error, 'Error occurred while calling tool', {
928
+ toolName: name,
929
+ toolStatus,
930
+ mcpSessionId,
931
+ failureCategory: callDiagnostics.failure_category,
932
+ failureHttpStatus: callDiagnostics.failure_http_status,
933
+ actorName: callDiagnostics.actor_name,
934
+ validationKeyword: callDiagnostics.validation_keyword,
935
+ validationPath: callDiagnostics.validation_path,
936
+ validationMissingProperty: callDiagnostics.validation_missing_property,
937
+ validationAdditionalProperty: callDiagnostics.validation_additional_property,
938
+ });
847
939
  const errorMessage = (error instanceof Error) ? error.message : 'Unknown error';
848
940
  return buildMCPResponse({
849
941
  texts: [`Error calling tool "${name}": ${errorMessage}. Please verify the tool name, input parameters, and ensure all required resources are available.`],
850
942
  isError: true,
851
- toolStatus,
943
+ telemetry: { toolStatus },
852
944
  });
853
945
  }
854
946
  finally {
855
- this.finalizeAndTrackTelemetry(telemetryData, userId, startTime, toolStatus);
947
+ if (shouldTrackTelemetry) {
948
+ this.finalizeAndTrackTelemetry(telemetryData, userId, startTime, toolStatus, callDiagnostics);
949
+ }
856
950
  }
857
951
  const availableTools = this.listToolNames();
858
- const msg = `Unknown tool type for "${name}".
859
- Available tools: ${availableTools.length > 0 ? availableTools.join(', ') : 'none'}.
860
- Please verify the tool name and ensure the tool is properly registered.`;
952
+ const msg = dedent `
953
+ Unknown tool type for "${name}".
954
+ Available tools: ${availableTools.length > 0 ? availableTools.join(', ') : 'none'}.
955
+ Please verify the tool name and ensure the tool is properly registered.
956
+ `;
861
957
  log.softFail(msg, { mcpSessionId, statusCode: 404 });
862
958
  await this.server.sendLoggingMessage({
863
959
  level: 'error',
@@ -874,8 +970,9 @@ Please verify the tool name and ensure the tool is properly registered.`;
874
970
  * @param userId - Apify user ID (string or null if not available)
875
971
  * @param startTime - Timestamp when the tool call started
876
972
  * @param toolStatus - Final status of the tool call
973
+ * @param callDiagnostics - Telemetry fields: always includes actor_name/actor_id when available; failure-specific fields only on non-success
877
974
  */
878
- finalizeAndTrackTelemetry(telemetryData, userId, startTime, toolStatus) {
975
+ finalizeAndTrackTelemetry(telemetryData, userId, startTime, toolStatus, callDiagnostics) {
879
976
  if (!telemetryData) {
880
977
  return;
881
978
  }
@@ -884,6 +981,8 @@ Please verify the tool name and ensure the tool is properly registered.`;
884
981
  ...telemetryData,
885
982
  tool_status: toolStatus,
886
983
  tool_exec_time_ms: execTime,
984
+ // Always include actor_name/actor_id; failure-specific fields are only present when callDiagnostics has them.
985
+ ...callDiagnostics,
887
986
  };
888
987
  trackToolCall(userId, this.telemetryEnv, finalizedTelemetryData);
889
988
  }
@@ -902,8 +1001,10 @@ Please verify the tool name and ensure the tool is properly registered.`;
902
1001
  */
903
1002
  async executeToolAndUpdateTask(params) {
904
1003
  var _a;
905
- const { taskId, tool, cleanArgs, logArgs, paymentErrorResult, apifyClient, apifyToken, progressToken, extra, mcpSessionId, userRentedActorIds, } = params;
1004
+ const { taskId, tool, toolArgs, logSafeArgs, paymentRequiredResult, apifyClient, apifyToken, progressToken, extra, mcpSessionId, actorName, actorId, userRentedActorIds, } = params;
906
1005
  let toolStatus = TOOL_STATUS.SUCCEEDED;
1006
+ // Always populate actor fields so they're tracked on both success and failure paths.
1007
+ let callDiagnostics = { ...buildActorFields(actorName, actorId) };
907
1008
  const startTime = Date.now();
908
1009
  log.debug('[executeToolAndUpdateTask] Starting task execution', {
909
1010
  taskId,
@@ -912,7 +1013,7 @@ Please verify the tool name and ensure the tool is properly registered.`;
912
1013
  });
913
1014
  // Prepare telemetry before try-catch so it's accessible to both paths.
914
1015
  // This avoids re-fetching user data in the error handler.
915
- const { telemetryData, userId } = await this.prepareTelemetryData(tool, mcpSessionId, apifyToken);
1016
+ const { telemetryData, userId } = await this.prepareTelemetryData(getToolFullName(tool), mcpSessionId, apifyToken);
916
1017
  try {
917
1018
  // Check if task was already cancelled before we start execution.
918
1019
  // Critical: if a client cancels the task immediately after creation (race condition),
@@ -923,7 +1024,9 @@ Please verify the tool name and ensure the tool is properly registered.`;
923
1024
  taskId,
924
1025
  mcpSessionId,
925
1026
  });
926
- this.finalizeAndTrackTelemetry(telemetryData, userId, startTime, TOOL_STATUS.ABORTED);
1027
+ this.finalizeAndTrackTelemetry(telemetryData, userId, startTime, TOOL_STATUS.ABORTED, {
1028
+ ...buildActorFields(actorName, actorId),
1029
+ });
927
1030
  return;
928
1031
  }
929
1032
  log.debug('[executeToolAndUpdateTask] Updating task status to working', {
@@ -933,10 +1036,15 @@ Please verify the tool name and ensure the tool is properly registered.`;
933
1036
  await this.taskStore.updateTaskStatus(taskId, 'working', undefined, mcpSessionId);
934
1037
  // Execute the tool and get the result
935
1038
  let result = {};
936
- // Check payment validation (already computed by preparePayment in the caller)
937
- if (paymentErrorResult) {
1039
+ // Check payment validation (already computed by prepareToolCallContext in the caller)
1040
+ if (paymentRequiredResult) {
938
1041
  toolStatus = TOOL_STATUS.SOFT_FAIL;
939
- result = paymentErrorResult;
1042
+ callDiagnostics = {
1043
+ failure_category: FAILURE_CATEGORY.INVALID_INPUT,
1044
+ failure_http_status: 402,
1045
+ ...buildActorFields(actorName, actorId),
1046
+ };
1047
+ result = paymentRequiredResult;
940
1048
  }
941
1049
  // Callback to propagate Actor run statusMessage into the task store.
942
1050
  // Clients retrieve it via tasks/get and tasks/list polling.
@@ -948,9 +1056,9 @@ Please verify the tool name and ensure the tool is properly registered.`;
948
1056
  if (toolStatus === TOOL_STATUS.SUCCEEDED && tool.type === 'internal') {
949
1057
  const progressTracker = createProgressTracker(progressToken, extra.sendNotification, taskId, onStatusMessage);
950
1058
  try {
951
- log.info('Calling internal tool for task', { taskId, name: tool.name, mcpSessionId, input: logArgs });
1059
+ log.info('Calling internal tool for task', { taskId, name: tool.name, mcpSessionId, input: logSafeArgs });
952
1060
  const res = await tool.call({
953
- args: cleanArgs,
1061
+ args: toolArgs,
954
1062
  extra,
955
1063
  apifyMcpServer: this,
956
1064
  mcpServer: this.server,
@@ -960,19 +1068,10 @@ Please verify the tool name and ensure the tool is properly registered.`;
960
1068
  progressTracker,
961
1069
  mcpSessionId,
962
1070
  });
963
- // If the tool returned internalToolStatus, use it; otherwise infer from isError flag
964
- const { internalToolStatus, ...rest } = res;
965
- if (internalToolStatus !== undefined) {
966
- toolStatus = internalToolStatus;
967
- }
968
- else if ('isError' in rest && rest.isError) {
969
- toolStatus = TOOL_STATUS.FAILED;
970
- }
971
- else {
972
- toolStatus = TOOL_STATUS.SUCCEEDED;
973
- }
974
- // Never expose internalToolStatus to MCP clients
975
- result = rest;
1071
+ const diag = extractToolTelemetry(res, actorName, actorId);
1072
+ toolStatus = diag.toolStatus;
1073
+ callDiagnostics = { ...callDiagnostics, ...diag.callDiagnostics };
1074
+ result = res;
976
1075
  }
977
1076
  finally {
978
1077
  if (progressTracker) {
@@ -984,10 +1083,10 @@ Please verify the tool name and ensure the tool is properly registered.`;
984
1083
  if (toolStatus === TOOL_STATUS.SUCCEEDED && tool.type === 'actor') {
985
1084
  const progressTracker = createProgressTracker(progressToken, extra.sendNotification, taskId, onStatusMessage);
986
1085
  try {
987
- log.info('Calling Actor for task', { taskId, actorName: tool.actorFullName, mcpSessionId, input: logArgs });
1086
+ log.info('Calling Actor for task', { taskId, actorName: tool.actorFullName, mcpSessionId, input: logSafeArgs });
988
1087
  const executorResult = await this.actorExecutor.executeActorTool({
989
1088
  actorFullName: tool.actorFullName,
990
- input: cleanArgs,
1089
+ input: toolArgs,
991
1090
  apifyClient,
992
1091
  callOptions: { memory: tool.memoryMbytes },
993
1092
  progressTracker,
@@ -1026,19 +1125,39 @@ Please verify the tool name and ensure the tool is properly registered.`;
1026
1125
  });
1027
1126
  await this.taskStore.storeTaskResult(taskId, 'completed', result, mcpSessionId);
1028
1127
  log.debug('Task completed successfully', { taskId, toolName: tool.name, mcpSessionId });
1029
- this.finalizeAndTrackTelemetry(telemetryData, userId, startTime, toolStatus);
1128
+ this.finalizeAndTrackTelemetry(telemetryData, userId, startTime, toolStatus, callDiagnostics);
1030
1129
  }
1031
1130
  catch (error) {
1032
- log.error('Error executing tool for task', { taskId, mcpSessionId, error });
1033
1131
  // Handle 402 Payment Required — return structured x402 result so clients can auto-pay
1034
1132
  const httpStatus = getHttpStatusCode(error);
1035
1133
  if (httpStatus === HTTP_PAYMENT_REQUIRED) {
1036
1134
  logHttpError(error, 'Payment required while calling tool (task mode)', { toolName: tool.name });
1037
1135
  await this.taskStore.storeTaskResult(taskId, 'completed', buildPaymentRequiredResponse(error), mcpSessionId);
1038
- this.finalizeAndTrackTelemetry(telemetryData, userId, startTime, TOOL_STATUS.SOFT_FAIL);
1136
+ this.finalizeAndTrackTelemetry(telemetryData, userId, startTime, TOOL_STATUS.SOFT_FAIL, {
1137
+ failure_category: FAILURE_CATEGORY.INVALID_INPUT,
1138
+ failure_http_status: 402,
1139
+ ...buildActorFields(actorName, actorId),
1140
+ });
1039
1141
  return;
1040
1142
  }
1041
1143
  toolStatus = getToolStatusFromError(error, Boolean((_a = extra.signal) === null || _a === void 0 ? void 0 : _a.aborted));
1144
+ const failureDetail = error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200);
1145
+ callDiagnostics = {
1146
+ failure_category: classifyFailureCategory(error),
1147
+ ...(httpStatus !== undefined ? { failure_http_status: httpStatus } : {}),
1148
+ failure_detail: failureDetail,
1149
+ ...buildActorFields(actorName, actorId),
1150
+ };
1151
+ log.error('Error executing tool for task', {
1152
+ taskId,
1153
+ toolName: tool.name,
1154
+ toolStatus,
1155
+ mcpSessionId,
1156
+ failureCategory: callDiagnostics.failure_category,
1157
+ failureHttpStatus: callDiagnostics.failure_http_status,
1158
+ actorName: callDiagnostics.actor_name,
1159
+ error,
1160
+ });
1042
1161
  const errorMessage = (error instanceof Error) ? error.message : 'Unknown error';
1043
1162
  // Check if task was cancelled before storing result
1044
1163
  // TODO: In future, we should actually stop execution via AbortController,
@@ -1048,7 +1167,7 @@ Please verify the tool name and ensure the tool is properly registered.`;
1048
1167
  taskId,
1049
1168
  mcpSessionId,
1050
1169
  });
1051
- this.finalizeAndTrackTelemetry(telemetryData, userId, startTime, toolStatus);
1170
+ this.finalizeAndTrackTelemetry(telemetryData, userId, startTime, toolStatus, callDiagnostics);
1052
1171
  return;
1053
1172
  }
1054
1173
  log.debug('[executeToolAndUpdateTask] Storing failed result', {
@@ -1064,18 +1183,17 @@ Please verify the tool name and ensure the tool is properly registered.`;
1064
1183
  isError: true,
1065
1184
  internalToolStatus: toolStatus,
1066
1185
  }, mcpSessionId);
1067
- this.finalizeAndTrackTelemetry(telemetryData, userId, startTime, toolStatus);
1186
+ this.finalizeAndTrackTelemetry(telemetryData, userId, startTime, toolStatus, callDiagnostics);
1068
1187
  }
1069
1188
  }
1070
1189
  /*
1071
1190
  * Creates telemetry data for a tool call.
1072
1191
  */
1073
- async prepareTelemetryData(tool, mcpSessionId, apifyToken) {
1192
+ async prepareTelemetryData(toolName, mcpSessionId, apifyToken) {
1074
1193
  var _a, _b, _c, _d, _e;
1075
1194
  if (!this.telemetryEnabled) {
1076
1195
  return { telemetryData: null, userId: null };
1077
1196
  }
1078
- const toolFullName = tool.type === 'actor' ? tool.actorFullName : tool.name;
1079
1197
  // Get userId from cache or fetch from API
1080
1198
  let userId = null;
1081
1199
  if (apifyToken) {
@@ -1094,7 +1212,7 @@ Please verify the tool name and ensure the tool is properly registered.`;
1094
1212
  mcp_client_capabilities: capabilities || null,
1095
1213
  mcp_session_id: mcpSessionId || '',
1096
1214
  transport_type: this.options.transportType || '',
1097
- tool_name: toolFullName,
1215
+ tool_name: toolName,
1098
1216
  tool_status: TOOL_STATUS.SUCCEEDED, // Will be updated in finally
1099
1217
  tool_exec_time_ms: 0, // Will be calculated in finally
1100
1218
  };