Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 32 additions & 25 deletions src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1035,15 +1035,45 @@ export class ActorsMcpServer {
mcpSessionId,
});

// Pre-flight failure is already known — the outcome needs no work. Resolve the
// task synchronously: store the failure as the terminal `completed` result and emit
// exactly one `completed` status notification (no `updateTaskStatus('working')`, so no
// `working` notification). Standby rejection wins over the generic payment-required
// short-circuit, matching the sync path's precedence. Telemetry rides the handler's
// outer `finally` (shouldTrackTelemetry stays true), firing once with the sync-path
// properties.
const preflightResult = standbyRejection ?? paymentRequiredResult;
if (preflightResult) {
toolStatus = TOOL_STATUS.SOFT_FAIL;
callDiagnostics = {
failure_category: FAILURE_CATEGORY.INVALID_INPUT,
...(standbyRejection ? {} : { failure_http_status: 402 }),
...buildActorFields(actorName, actorId),
};
await storeTaskResultOrSkipIfExpired(
this.taskStore,
tool.name,
task.taskId,
'completed',
preflightResult,
mcpSessionId,
);
await emitTaskStatusNotification(task.taskId, mcpSessionId, this.taskStore, this.server);
captureResult(preflightResult);
// createTask returned status `working`; synthesize the terminal status instead of
// re-fetching — if the task expired before the result store (the one case
// storeTaskResultOrSkipIfExpired tolerates), a re-fetch would come back empty and a
// `working` fallback would contradict the tasks/get 404 the client sees next.
return { task: { ...task, status: 'completed' as const } };
}

// Execute the tool asynchronously and update task status
setImmediate(() => {
this.executeToolAndUpdateTask({
taskId: task.taskId,
tool,
toolArgs: toolArgs!,
logSafeArgs,
standbyRejection,
paymentRequiredResult,
apifyClient: apifyClient!,
apifyToken,
progressToken,
Expand Down Expand Up @@ -1426,8 +1456,6 @@ export class ActorsMcpServer {
tool: ToolEntry;
toolArgs: Record<string, unknown>;
logSafeArgs: unknown;
standbyRejection?: Record<string, unknown> | null;
paymentRequiredResult?: Record<string, unknown>;
apifyClient: ApifyClient;
apifyToken: string;
progressToken: string | number | undefined;
Expand All @@ -1441,8 +1469,6 @@ export class ActorsMcpServer {
tool,
toolArgs,
logSafeArgs,
standbyRejection,
paymentRequiredResult,
apifyClient,
apifyToken,
progressToken,
Expand Down Expand Up @@ -1538,25 +1564,6 @@ export class ActorsMcpServer {
// Execute the tool and get the result
let result: Record<string, unknown> = {};

// Mirror the sync-path ordering: standby rejection wins over the generic 402 so the
// stored task result carries the precise reason the agent should act on.
if (standbyRejection) {
toolStatus = TOOL_STATUS.SOFT_FAIL;
callDiagnostics = {
failure_category: FAILURE_CATEGORY.INVALID_INPUT,
...buildActorFields(actorName, actorId),
};
result = standbyRejection;
} else if (paymentRequiredResult) {
toolStatus = TOOL_STATUS.SOFT_FAIL;
callDiagnostics = {
failure_category: FAILURE_CATEGORY.INVALID_INPUT,
failure_http_status: 402,
...buildActorFields(actorName, actorId),
};
result = paymentRequiredResult;
}

// Callback to propagate Actor run statusMessage into the task store and emit a push notification.
// TODO(TC-3): cancel arriving while this is scheduled throws cancelled → working;
// currently swallowed by progress.ts's tick catch — guard or catch explicitly.
Expand Down
10 changes: 8 additions & 2 deletions tests/unit/helpers/mcp_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,16 @@ export function makeThrowingTool(
/**
* A synthetic internal tool that records what the server passed into `call` (whether it ran, and the
* `progressTracker` it received). Generalizes to any "did the server pass X to the tool?" assertion.
* `paymentRequired`/`taskSupport` let a caller drive the pre-flight payment/task paths.
*/
export function makeRecorderTool(name: string): {
export function makeRecorderTool(
name: string,
options: { paymentRequired?: boolean; taskSupport?: (typeof ALLOWED_TASK_TOOL_EXECUTION_MODES)[number] } = {},
): {
tool: ToolEntry;
received: { called: boolean; progressTracker: InternalToolArgs['progressTracker'] | undefined };
} {
const { paymentRequired = false, taskSupport } = options;
const received: { called: boolean; progressTracker: InternalToolArgs['progressTracker'] | undefined } = {
called: false,
progressTracker: undefined,
Expand All @@ -93,8 +98,9 @@ export function makeRecorderTool(name: string): {
description: 'recorder tool for progress wiring tests',
inputSchema: { type: 'object', properties: {}, additionalProperties: true },
ajvValidate: Object.assign(() => true, { errors: null }) as unknown as ToolEntry['ajvValidate'],
paymentRequired: false,
paymentRequired,
annotations: {},
...(taskSupport ? { execution: { taskSupport } } : {}),
call: async (toolArgs: InternalToolArgs) => {
received.called = true;
received.progressTracker = toolArgs.progressTracker;
Expand Down
Loading
Loading