Skip to content

Commit a6b38c8

Browse files
committed
Add cancel_run tool
1 parent 51e4716 commit a6b38c8

File tree

4 files changed

+149
-44
lines changed

4 files changed

+149
-44
lines changed
Lines changed: 33 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,47 @@
1-
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
21
import { json } from "@remix-run/server-runtime";
32
import { z } from "zod";
4-
import { prisma } from "~/db.server";
5-
import { authenticateApiRequest } from "~/services/apiAuth.server";
3+
import { $replica } from "~/db.server";
4+
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
65
import { CancelTaskRunService } from "~/v3/services/cancelTaskRun.server";
76

87
const ParamsSchema = z.object({
98
runParam: z.string(),
109
});
1110

12-
export async function action({ request, params }: ActionFunctionArgs) {
13-
// Ensure this is a POST request
14-
if (request.method.toUpperCase() !== "POST") {
15-
return { status: 405, body: "Method Not Allowed" };
16-
}
17-
18-
// Authenticate the request
19-
const authenticationResult = await authenticateApiRequest(request);
20-
21-
if (!authenticationResult) {
22-
return json({ error: "Invalid or Missing API Key" }, { status: 401 });
23-
}
24-
25-
const parsed = ParamsSchema.safeParse(params);
26-
27-
if (!parsed.success) {
28-
return json({ error: "Invalid or Missing run id" }, { status: 400 });
29-
}
30-
31-
const { runParam } = parsed.data;
32-
33-
const taskRun = await prisma.taskRun.findUnique({
34-
where: {
35-
friendlyId: runParam,
36-
runtimeEnvironmentId: authenticationResult.environment.id,
11+
const { action } = createActionApiRoute(
12+
{
13+
params: ParamsSchema,
14+
allowJWT: true,
15+
corsStrategy: "none",
16+
authorization: {
17+
action: "write",
18+
resource: (params) => ({ runs: params.runParam }),
19+
superScopes: ["write:runs", "admin"],
3720
},
38-
});
21+
findResource: async (params, auth) => {
22+
return $replica.taskRun.findFirst({
23+
where: {
24+
friendlyId: params.runParam,
25+
runtimeEnvironmentId: auth.environment.id,
26+
},
27+
});
28+
},
29+
},
30+
async ({ resource }) => {
31+
if (!resource) {
32+
return json({ error: "Run not found" }, { status: 404 });
33+
}
3934

40-
if (!taskRun) {
41-
return json({ error: "Run not found" }, { status: 404 });
42-
}
35+
const service = new CancelTaskRunService();
4336

44-
const service = new CancelTaskRunService();
37+
try {
38+
await service.call(resource);
39+
} catch (error) {
40+
return json({ error: "Internal Server Error" }, { status: 500 });
41+
}
4542

46-
try {
47-
await service.call(taskRun);
48-
} catch (error) {
49-
return json({ error: "Internal Server Error" }, { status: 500 });
43+
return json({ id: resource.friendlyId }, { status: 200 });
5044
}
45+
);
5146

52-
return json({ id: runParam }, { status: 200 });
53-
}
47+
export { action };

apps/webapp/app/services/routeBuilders/apiBuilder.server.ts

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -430,14 +430,26 @@ type ApiKeyActionRouteBuilderOptions<
430430
TParamsSchema extends AnyZodSchema | undefined = undefined,
431431
TSearchParamsSchema extends AnyZodSchema | undefined = undefined,
432432
THeadersSchema extends AnyZodSchema | undefined = undefined,
433-
TBodySchema extends AnyZodSchema | undefined = undefined
433+
TBodySchema extends AnyZodSchema | undefined = undefined,
434+
TResource = never
434435
> = {
435436
params?: TParamsSchema;
436437
searchParams?: TSearchParamsSchema;
437438
headers?: THeadersSchema;
438439
allowJWT?: boolean;
439440
corsStrategy?: "all" | "none";
440441
method?: "POST" | "PUT" | "DELETE" | "PATCH";
442+
findResource?: (
443+
params: TParamsSchema extends z.ZodFirstPartySchemaTypes | z.ZodDiscriminatedUnion<any, any>
444+
? z.infer<TParamsSchema>
445+
: undefined,
446+
authentication: ApiAuthenticationResultSuccess,
447+
searchParams: TSearchParamsSchema extends
448+
| z.ZodFirstPartySchemaTypes
449+
| z.ZodDiscriminatedUnion<any, any>
450+
? z.infer<TSearchParamsSchema>
451+
: undefined
452+
) => Promise<TResource | undefined>;
441453
authorization?: {
442454
action: AuthorizationAction;
443455
resource: (
@@ -466,7 +478,8 @@ type ApiKeyActionHandlerFunction<
466478
TParamsSchema extends AnyZodSchema | undefined,
467479
TSearchParamsSchema extends AnyZodSchema | undefined,
468480
THeadersSchema extends AnyZodSchema | undefined = undefined,
469-
TBodySchema extends AnyZodSchema | undefined = undefined
481+
TBodySchema extends AnyZodSchema | undefined = undefined,
482+
TResource = never
470483
> = (args: {
471484
params: TParamsSchema extends z.ZodFirstPartySchemaTypes | z.ZodDiscriminatedUnion<any, any>
472485
? z.infer<TParamsSchema>
@@ -484,25 +497,29 @@ type ApiKeyActionHandlerFunction<
484497
: undefined;
485498
authentication: ApiAuthenticationResultSuccess;
486499
request: Request;
500+
resource?: TResource;
487501
}) => Promise<Response>;
488502

489503
export function createActionApiRoute<
490504
TParamsSchema extends AnyZodSchema | undefined = undefined,
491505
TSearchParamsSchema extends AnyZodSchema | undefined = undefined,
492506
THeadersSchema extends AnyZodSchema | undefined = undefined,
493-
TBodySchema extends AnyZodSchema | undefined = undefined
507+
TBodySchema extends AnyZodSchema | undefined = undefined,
508+
TResource = never
494509
>(
495510
options: ApiKeyActionRouteBuilderOptions<
496511
TParamsSchema,
497512
TSearchParamsSchema,
498513
THeadersSchema,
499-
TBodySchema
514+
TBodySchema,
515+
TResource
500516
>,
501517
handler: ApiKeyActionHandlerFunction<
502518
TParamsSchema,
503519
TSearchParamsSchema,
504520
THeadersSchema,
505-
TBodySchema
521+
TBodySchema,
522+
TResource
506523
>
507524
) {
508525
const {
@@ -682,13 +699,18 @@ export function createActionApiRoute<
682699
}
683700
}
684701

702+
const resource = options.findResource
703+
? await options.findResource(parsedParams, authenticationResult, parsedSearchParams)
704+
: undefined;
705+
685706
const result = await handler({
686707
params: parsedParams,
687708
searchParams: parsedSearchParams,
688709
headers: parsedHeaders,
689710
body: parsedBody,
690711
authentication: authenticationResult,
691712
request,
713+
resource,
692714
});
693715
return await wrapResponse(request, result, corsStrategy !== "none");
694716
} catch (error) {

packages/cli-v3/src/commands/mcp.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { CLOUD_API_URL } from "../consts.js";
1010
import { McpContext } from "../mcp/context.js";
1111
import { FileLogger } from "../mcp/logger.js";
1212
import {
13+
registerCancelRunTool,
1314
registerCreateProjectTool,
1415
registerDeployTool,
1516
registerGetRunDetailsTool,
@@ -105,6 +106,7 @@ export async function mcpCommand(options: McpCommandOptions) {
105106
registerGetTasksTool(context);
106107
registerTriggerTaskTool(context);
107108
registerGetRunDetailsTool(context);
109+
registerCancelRunTool(context);
108110
registerListRunsTool(context);
109111
registerListProjectsTool(context);
110112
registerListOrgsTool(context);

packages/cli-v3/src/mcp/tools.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { performSearch } from "./mintlifyClient.js";
1919
import { ProjectRefSchema } from "./schemas.js";
2020
import { respondWithError } from "./utils.js";
2121
import { resolveSync as esmResolve } from "mlly";
22+
import { tryCatch } from "@trigger.dev/core/utils";
2223

2324
export function registerListProjectsTool(context: McpContext) {
2425
context.server.registerTool(
@@ -618,6 +619,92 @@ export function registerGetRunDetailsTool(context: McpContext) {
618619
);
619620
}
620621

622+
export function registerCancelRunTool(context: McpContext) {
623+
context.server.registerTool(
624+
"cancel_run",
625+
{
626+
description: "Cancel a run",
627+
inputSchema: {
628+
runId: z.string().describe("The ID of the run to cancel, starts with run_"),
629+
projectRef: ProjectRefSchema,
630+
configPath: z
631+
.string()
632+
.describe(
633+
"The path to the trigger.config.ts file. Only used when the trigger.config.ts file is not at the root dir (like in a monorepo setup). If not provided, we will try to find the config file in the current working directory"
634+
)
635+
.optional(),
636+
environment: z
637+
.enum(["dev", "staging", "prod", "preview"])
638+
.describe("The environment to trigger the task in")
639+
.default("dev"),
640+
branch: z
641+
.string()
642+
.describe("The branch to trigger the task in, only used for preview environments")
643+
.optional(),
644+
},
645+
},
646+
async ({ projectRef, configPath, environment, branch, runId }) => {
647+
context.logger?.log("calling cancel_run", {
648+
projectRef,
649+
configPath,
650+
environment,
651+
branch,
652+
runId,
653+
});
654+
655+
if (context.options.devOnly && environment !== "dev") {
656+
return respondWithError(
657+
`This MCP server is only available for the dev environment. You tried to access the ${environment} environment. Remove the --dev-only flag to access other environments.`
658+
);
659+
}
660+
661+
const projectRefResult = await resolveExistingProjectRef(context, projectRef, configPath);
662+
663+
if (projectRefResult.status === "error") {
664+
return respondWithError(projectRefResult.error);
665+
}
666+
667+
const $projectRef = projectRefResult.projectRef;
668+
669+
context.logger?.log("cancel_run projectRefResult", { projectRefResult });
670+
671+
const auth = await mcpAuth({
672+
server: context.server,
673+
defaultApiUrl: context.options.apiUrl,
674+
profile: context.options.profile,
675+
context,
676+
});
677+
678+
if (!auth.ok) {
679+
return respondWithError(auth.error);
680+
}
681+
682+
const apiClient = await createApiClientWithPublicJWT(auth, $projectRef, environment, [
683+
`write:runs:${runId}`,
684+
`read:runs:${runId}`,
685+
]);
686+
687+
if (!apiClient) {
688+
return respondWithError("Failed to create API client with public JWT");
689+
}
690+
691+
const [cancelError] = await tryCatch(apiClient.cancelRun(runId));
692+
693+
if (cancelError) {
694+
return respondWithError(cancelError.message);
695+
}
696+
697+
const retrieveResult = await apiClient.retrieveRun(runId);
698+
699+
const runUrl = `${auth.dashboardUrl}/projects/v3/${$projectRef}/runs/${runId}`;
700+
701+
return {
702+
content: [{ type: "text", text: JSON.stringify({ ...retrieveResult, runUrl }, null, 2) }],
703+
};
704+
}
705+
);
706+
}
707+
621708
export function registerListRunsTool(context: McpContext) {
622709
context.server.registerTool(
623710
"list_runs",

0 commit comments

Comments
 (0)