Skip to content

Commit bbf2597

Browse files
committed
Adding getTasks and triggerTask tools
1 parent f8126cd commit bbf2597

File tree

9 files changed

+626
-41
lines changed

9 files changed

+626
-41
lines changed
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { json, type LoaderFunctionArgs } from "@remix-run/server-runtime";
2+
import { generateJWT as internal_generateJWT } from "@trigger.dev/core/v3";
3+
import { z } from "zod";
4+
import { prisma } from "~/db.server";
5+
import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server";
6+
import { getEnvironmentFromEnv } from "./api.v1.projects.$projectRef.$env";
7+
8+
const ParamsSchema = z.object({
9+
projectRef: z.string(),
10+
env: z.enum(["dev", "staging", "prod", "preview"]),
11+
});
12+
13+
type ParamsSchema = z.infer<typeof ParamsSchema>;
14+
15+
const RequestBodySchema = z.object({
16+
claims: z
17+
.object({
18+
scopes: z.array(z.string()).default([]),
19+
})
20+
.optional(),
21+
expirationTime: z.union([z.number(), z.string()]).optional(),
22+
});
23+
24+
export async function action({ request, params }: LoaderFunctionArgs) {
25+
const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request);
26+
27+
if (!authenticationResult) {
28+
return json({ error: "Invalid or Missing Access Token" }, { status: 401 });
29+
}
30+
31+
const parsedParams = ParamsSchema.safeParse(params);
32+
33+
if (!parsedParams.success) {
34+
return json({ error: "Invalid Params" }, { status: 400 });
35+
}
36+
37+
const { projectRef, env } = parsedParams.data;
38+
39+
const project = await prisma.project.findFirst({
40+
where: {
41+
externalRef: projectRef,
42+
organization: {
43+
members: {
44+
some: {
45+
userId: authenticationResult.userId,
46+
},
47+
},
48+
},
49+
},
50+
});
51+
52+
if (!project) {
53+
return json({ error: "Project not found" }, { status: 404 });
54+
}
55+
56+
const envResult = await getEnvironmentFromEnv({
57+
projectId: project.id,
58+
userId: authenticationResult.userId,
59+
env,
60+
});
61+
62+
if (!envResult.success) {
63+
return json({ error: envResult.error }, { status: 404 });
64+
}
65+
66+
const runtimeEnv = envResult.environment;
67+
68+
const parsedBody = RequestBodySchema.safeParse(await request.json());
69+
70+
if (!parsedBody.success) {
71+
return json(
72+
{ error: "Invalid request body", issues: parsedBody.error.issues },
73+
{ status: 400 }
74+
);
75+
}
76+
77+
const claims = {
78+
sub: runtimeEnv.id,
79+
pub: true,
80+
...parsedBody.data.claims,
81+
};
82+
83+
const jwt = await internal_generateJWT({
84+
secretKey: runtimeEnv.apiKey,
85+
payload: claims,
86+
expirationTime: parsedBody.data.expirationTime ?? "1h",
87+
});
88+
89+
return json({ token: jwt });
90+
}

apps/webapp/app/routes/api.v1.projects.$projectRef.$env.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
7070
return json(result);
7171
}
7272

73-
async function getEnvironmentFromEnv({
73+
export async function getEnvironmentFromEnv({
7474
projectId,
7575
userId,
7676
env,
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { json, type LoaderFunctionArgs } from "@remix-run/server-runtime";
2+
import { z } from "zod";
3+
import { $replica, prisma } from "~/db.server";
4+
import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server";
5+
import { findCurrentWorkerFromEnvironment } from "~/v3/models/workerDeployment.server";
6+
import { getEnvironmentFromEnv } from "./api.v1.projects.$projectRef.$env";
7+
import { GetWorkerByTagResponse } from "@trigger.dev/core/v3/schemas";
8+
9+
const ParamsSchema = z.object({
10+
projectRef: z.string(),
11+
tagName: z.string(),
12+
env: z.enum(["dev", "staging", "prod", "preview"]),
13+
});
14+
15+
type ParamsSchema = z.infer<typeof ParamsSchema>;
16+
17+
export async function loader({ request, params }: LoaderFunctionArgs) {
18+
const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request);
19+
20+
if (!authenticationResult) {
21+
return json({ error: "Invalid or Missing Access Token" }, { status: 401 });
22+
}
23+
24+
const parsedParams = ParamsSchema.safeParse(params);
25+
26+
if (!parsedParams.success) {
27+
return json({ error: "Invalid Params" }, { status: 400 });
28+
}
29+
30+
const { projectRef, env } = parsedParams.data;
31+
32+
const project = await prisma.project.findFirst({
33+
where: {
34+
externalRef: projectRef,
35+
organization: {
36+
members: {
37+
some: {
38+
userId: authenticationResult.userId,
39+
},
40+
},
41+
},
42+
},
43+
});
44+
45+
if (!project) {
46+
return json({ error: "Project not found" }, { status: 404 });
47+
}
48+
49+
const envResult = await getEnvironmentFromEnv({
50+
projectId: project.id,
51+
userId: authenticationResult.userId,
52+
env,
53+
});
54+
55+
if (!envResult.success) {
56+
return json({ error: envResult.error }, { status: 404 });
57+
}
58+
59+
const runtimeEnv = envResult.environment;
60+
61+
const currentWorker = await findCurrentWorkerFromEnvironment(
62+
{
63+
id: runtimeEnv.id,
64+
type: runtimeEnv.type,
65+
},
66+
$replica,
67+
params.tagName
68+
);
69+
70+
if (!currentWorker) {
71+
return json({ error: "Worker not found" }, { status: 404 });
72+
}
73+
74+
const tasks = await $replica.backgroundWorkerTask.findMany({
75+
where: {
76+
workerId: currentWorker.id,
77+
},
78+
select: {
79+
friendlyId: true,
80+
slug: true,
81+
filePath: true,
82+
triggerSource: true,
83+
createdAt: true,
84+
payloadSchema: true,
85+
},
86+
orderBy: {
87+
slug: "asc",
88+
},
89+
});
90+
91+
// Prepare the response object
92+
const response: GetWorkerByTagResponse = {
93+
worker: {
94+
id: currentWorker.friendlyId,
95+
version: currentWorker.version,
96+
engine: currentWorker.engine,
97+
sdkVersion: currentWorker.sdkVersion,
98+
cliVersion: currentWorker.cliVersion,
99+
tasks: tasks.map((task) => ({
100+
id: task.friendlyId,
101+
slug: task.slug,
102+
filePath: task.filePath,
103+
triggerSource: task.triggerSource,
104+
createdAt: task.createdAt,
105+
payloadSchema: task.payloadSchema,
106+
})),
107+
},
108+
};
109+
110+
// Optionally validate the response before returning (for type safety)
111+
// WorkerResponseSchema.parse(response);
112+
113+
return json(response);
114+
}

packages/cli-v3/src/apiClient.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ import {
3333
WorkersListResponseBody,
3434
CreateProjectRequestBody,
3535
GetOrgsResponseBody,
36+
GetWorkerByTagResponse,
37+
GetJWTRequestBody,
38+
GetJWTResponse,
3639
} from "@trigger.dev/core/v3";
3740
import {
3841
WorkloadDebugLogRequestBody,
@@ -163,6 +166,36 @@ export class CliApiClient {
163166
});
164167
}
165168

169+
async getWorkerByTag(projectRef: string, envName: string, tagName: string = "current") {
170+
if (!this.accessToken) {
171+
throw new Error("getWorkerByTag: No access token");
172+
}
173+
174+
return wrapZodFetch(
175+
GetWorkerByTagResponse,
176+
`${this.apiURL}/api/v1/projects/${projectRef}/${envName}/workers/${tagName}`,
177+
{
178+
headers: this.getHeaders(),
179+
}
180+
);
181+
}
182+
183+
async getJWT(projectRef: string, envName: string, body: GetJWTRequestBody) {
184+
if (!this.accessToken) {
185+
throw new Error("getJWT: No access token");
186+
}
187+
188+
return wrapZodFetch(
189+
GetJWTResponse,
190+
`${this.apiURL}/api/v1/projects/${projectRef}/${envName}/jwt`,
191+
{
192+
method: "POST",
193+
headers: this.getHeaders(),
194+
body: JSON.stringify(body),
195+
}
196+
);
197+
}
198+
166199
async createBackgroundWorker(projectRef: string, body: CreateBackgroundWorkerRequestBody) {
167200
if (!this.accessToken) {
168201
throw new Error("createBackgroundWorker: No access token");

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,19 @@ import { McpContext } from "../mcp/context.js";
88
import { FileLogger } from "../mcp/logger.js";
99
import {
1010
registerCreateProjectTool,
11-
registerGetProjectDetailsTool,
11+
registerGetTasksTool,
1212
registerInitializeProjectTool,
1313
registerListOrgsTool,
1414
registerListProjectsTool,
1515
registerSearchDocsTool,
16+
registerTriggerTaskTool,
1617
} from "../mcp/tools.js";
1718
import { logger } from "../utilities/logger.js";
1819

1920
const McpCommandOptions = CommonCommandOptions.extend({
2021
projectRef: z.string().optional(),
2122
logFile: z.string().optional(),
23+
devOnly: z.boolean().default(false),
2224
});
2325

2426
export type McpCommandOptions = z.infer<typeof McpCommandOptions>;
@@ -29,6 +31,10 @@ export function configureMcpCommand(program: Command) {
2931
.command("mcp")
3032
.description("Run the MCP server")
3133
.option("-p, --project-ref <project ref>", "The project ref to use")
34+
.option(
35+
"--dev-only",
36+
"Only run the MCP server for the dev environment. Attempts to access other environments will fail."
37+
)
3238
.option("--log-file <log file>", "The file to log to")
3339
).action(async (options) => {
3440
wrapCommandAction("mcp", McpCommandOptions, options, async (opts) => {
@@ -62,7 +68,8 @@ export async function mcpCommand(options: McpCommandOptions) {
6268

6369
registerSearchDocsTool(context);
6470
registerInitializeProjectTool(context);
65-
registerGetProjectDetailsTool(context);
71+
registerGetTasksTool(context);
72+
registerTriggerTaskTool(context);
6673
registerListProjectsTool(context);
6774
registerListOrgsTool(context);
6875
registerCreateProjectTool(context);

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

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ import {
77
isPersonalAccessToken,
88
NotPersonalAccessTokenError,
99
} from "../utilities/isPersonalAccessToken.js";
10-
import { LoginResult } from "../utilities/session.js";
10+
import { LoginResult, LoginResultOk } from "../utilities/session.js";
1111
import { getPersonalAccessToken } from "../commands/login.js";
1212
import open from "open";
1313
import pRetry from "p-retry";
1414
import { McpContext } from "./context.js";
15+
import { ApiClient } from "@trigger.dev/core/v3";
1516

1617
export type McpAuthOptions = {
1718
server: McpServer;
@@ -177,3 +178,24 @@ async function askForLoginPermission(server: McpServer, authorizationCodeUrl: st
177178

178179
return result.action === "accept" && result.content?.allowLogin;
179180
}
181+
182+
export async function createApiClientWithPublicJWT(
183+
auth: LoginResultOk,
184+
projectRef: string,
185+
envName: string,
186+
scopes: string[]
187+
) {
188+
const cliApiClient = new CliApiClient(auth.auth.apiUrl, auth.auth.accessToken);
189+
190+
const jwt = await cliApiClient.getJWT(projectRef, envName, {
191+
claims: {
192+
scopes,
193+
},
194+
});
195+
196+
if (!jwt.success) {
197+
return;
198+
}
199+
200+
return new ApiClient(auth.auth.apiUrl, jwt.data.token);
201+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export type McpContextOptions = {
66
fileLogger?: FileLogger;
77
apiUrl?: string;
88
profile?: string;
9+
devOnly?: boolean;
910
};
1011

1112
export class McpContext {

0 commit comments

Comments
 (0)