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