Skip to content

Commit f086626

Browse files
authored
feat(cli): MCP server 2.0 (#2384)
1 parent 8483900 commit f086626

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+8109
-195
lines changed

.changeset/sharp-dolls-burn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"trigger.dev": patch
3+
---
4+
5+
feat: Add official MCP server, install MCP and rules CLI commands and wizards

.cursor/mcp.json

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
11
{
2-
"mcpServers": {
3-
"trigger.dev": {
4-
"url": "http://localhost:3333/sse"
5-
}
6-
}
7-
}
2+
"mcpServers": {}
3+
}

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,5 @@ apps/**/public/build
6363
/packages/core/src/package.json
6464
/packages/trigger-sdk/src/package.json
6565
/packages/python/src/package.json
66-
.claude
66+
.claude
67+
.mcp.log

apps/webapp/app/routes/account.authorization-code.$authorizationCode/route.tsx

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,25 @@
11
import { CheckCircleIcon } from "@heroicons/react/24/solid";
22
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
3-
import { title } from "process";
43
import { typedjson, useTypedLoaderData } from "remix-typedjson";
54
import { z } from "zod";
6-
import { ErrorIcon } from "~/assets/icons/ErrorIcon";
75
import { AppContainer, MainCenteredContainer } from "~/components/layout/AppLayout";
8-
import { LinkButton } from "~/components/primitives/Buttons";
96
import { Callout } from "~/components/primitives/Callout";
107
import { Header1 } from "~/components/primitives/Headers";
118
import { Icon } from "~/components/primitives/Icon";
129
import { Paragraph } from "~/components/primitives/Paragraph";
1310
import { logger } from "~/services/logger.server";
1411
import { createPersonalAccessTokenFromAuthorizationCode } from "~/services/personalAccessToken.server";
1512
import { requireUserId } from "~/services/session.server";
16-
import { rootPath } from "~/utils/pathBuilder";
1713

1814
const ParamsSchema = z.object({
1915
authorizationCode: z.string(),
2016
});
2117

18+
const SearchParamsSchema = z.object({
19+
source: z.string().optional(),
20+
clientName: z.string().optional(),
21+
});
22+
2223
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
2324
const userId = await requireUserId(request);
2425

@@ -32,13 +33,23 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
3233
});
3334
}
3435

36+
const url = new URL(request.url);
37+
const searchObject = Object.fromEntries(url.searchParams.entries());
38+
39+
const searchParams = SearchParamsSchema.safeParse(searchObject);
40+
41+
const source = (searchParams.success ? searchParams.data.source : undefined) ?? "cli";
42+
const clientName = (searchParams.success ? searchParams.data.clientName : undefined) ?? "unknown";
43+
3544
try {
3645
const personalAccessToken = await createPersonalAccessTokenFromAuthorizationCode(
3746
parsedParams.data.authorizationCode,
3847
userId
3948
);
4049
return typedjson({
4150
success: true as const,
51+
source,
52+
clientName,
4253
});
4354
} catch (error) {
4455
if (error instanceof Response) {
@@ -49,6 +60,8 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
4960
return typedjson({
5061
success: false as const,
5162
error: error.message,
63+
source,
64+
clientName,
5265
});
5366
}
5467

@@ -73,7 +86,7 @@ export default function Page() {
7386
<Icon icon={CheckCircleIcon} className="h-6 w-6 text-emerald-500" /> Successfully
7487
authenticated
7588
</Header1>
76-
<Paragraph>Return to your terminal to continue.</Paragraph>
89+
<Paragraph>{getInstructionsForSource(result.source, result.clientName)}</Paragraph>
7790
</div>
7891
) : (
7992
<div>
@@ -91,3 +104,21 @@ export default function Page() {
91104
</AppContainer>
92105
);
93106
}
107+
108+
const prettyClientNames: Record<string, string> = {
109+
"claude-code": "Claude Code",
110+
"cursor-vscode": "Cursor",
111+
"Visual Studio Code": "VSCode",
112+
"windsurf-client": "Windsurf",
113+
"claude-ai": "Claude Desktop",
114+
};
115+
116+
function getInstructionsForSource(source: string, clientName: string) {
117+
if (source === "mcp") {
118+
if (clientName) {
119+
return `Return to your ${prettyClientNames[clientName] ?? clientName} to continue.`;
120+
}
121+
}
122+
123+
return `Return to your terminal to continue.`;
124+
}

apps/webapp/app/routes/api.v1.deployments.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import { ActionFunctionArgs, json } from "@remix-run/server-runtime";
22
import {
3+
ApiDeploymentListSearchParams,
34
InitializeDeploymentRequestBody,
45
InitializeDeploymentResponseBody,
56
} from "@trigger.dev/core/v3";
7+
import { $replica } from "~/db.server";
68
import { authenticateApiRequest } from "~/services/apiAuth.server";
79
import { logger } from "~/services/logger.server";
10+
import { createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server";
811
import { ServiceValidationError } from "~/v3/services/baseService.server";
912
import { InitializeDeploymentService } from "~/v3/services/initializeDeployment.server";
1013

@@ -60,3 +63,119 @@ export async function action({ request, params }: ActionFunctionArgs) {
6063
}
6164
}
6265
}
66+
67+
export const loader = createLoaderApiRoute(
68+
{
69+
searchParams: ApiDeploymentListSearchParams,
70+
allowJWT: true,
71+
corsStrategy: "none",
72+
authorization: {
73+
action: "read",
74+
resource: () => ({ deployments: "list" }),
75+
superScopes: ["read:deployments", "read:all", "admin"],
76+
},
77+
findResource: async () => 1, // This is a dummy function, we don't need to find a resource
78+
},
79+
async ({ searchParams, authentication }) => {
80+
const limit = Math.max(Math.min(searchParams["page[size]"] ?? 20, 100), 5);
81+
82+
const afterDeployment = searchParams["page[after]"]
83+
? await $replica.workerDeployment.findFirst({
84+
where: {
85+
friendlyId: searchParams["page[after]"],
86+
environmentId: authentication.environment.id,
87+
},
88+
})
89+
: undefined;
90+
91+
const deployments = await $replica.workerDeployment.findMany({
92+
where: {
93+
environmentId: authentication.environment.id,
94+
...(afterDeployment ? { id: { lt: afterDeployment.id } } : {}),
95+
...getCreatedAtFilter(searchParams),
96+
...(searchParams.status ? { status: searchParams.status } : {}),
97+
},
98+
orderBy: {
99+
id: "desc",
100+
},
101+
take: limit + 1,
102+
});
103+
104+
const hasMore = deployments.length > limit;
105+
const nextCursor = hasMore ? deployments[limit - 1].friendlyId : undefined;
106+
const data = hasMore ? deployments.slice(0, limit) : deployments;
107+
108+
return json({
109+
data: data.map((deployment) => ({
110+
id: deployment.friendlyId,
111+
createdAt: deployment.createdAt,
112+
shortCode: deployment.shortCode,
113+
version: deployment.version.toString(),
114+
runtime: deployment.runtime,
115+
runtimeVersion: deployment.runtimeVersion,
116+
status: deployment.status,
117+
deployedAt: deployment.deployedAt,
118+
git: deployment.git,
119+
error: deployment.errorData ?? undefined,
120+
})),
121+
pagination: {
122+
next: nextCursor,
123+
},
124+
});
125+
}
126+
);
127+
128+
import parseDuration from "parse-duration";
129+
import { parseDate } from "@trigger.dev/core/v3/isomorphic";
130+
131+
function getCreatedAtFilter(searchParams: ApiDeploymentListSearchParams) {
132+
if (searchParams.period) {
133+
const duration = parseDuration(searchParams.period, "ms");
134+
135+
if (!duration) {
136+
throw new ServiceValidationError(
137+
`Invalid search query parameter: period=${searchParams.period}`,
138+
400
139+
);
140+
}
141+
142+
return {
143+
createdAt: {
144+
gte: new Date(Date.now() - duration),
145+
lte: new Date(),
146+
},
147+
};
148+
}
149+
150+
if (searchParams.from && searchParams.to) {
151+
const fromDate = safeDateFromString(searchParams.from, "from");
152+
const toDate = safeDateFromString(searchParams.to, "to");
153+
154+
return {
155+
createdAt: {
156+
gte: fromDate,
157+
lte: toDate,
158+
},
159+
};
160+
}
161+
162+
if (searchParams.from) {
163+
const fromDate = safeDateFromString(searchParams.from, "from");
164+
return {
165+
createdAt: {
166+
gte: fromDate,
167+
},
168+
};
169+
}
170+
171+
return {};
172+
}
173+
174+
function safeDateFromString(value: string, paramName: string) {
175+
const date = parseDate(value);
176+
177+
if (!date) {
178+
throw new ServiceValidationError(`Invalid search query parameter: ${paramName}=${value}`, 400);
179+
}
180+
return date;
181+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/server-runtime";
2+
import { json } from "@remix-run/server-runtime";
3+
import {
4+
CreateProjectRequestBody,
5+
GetProjectResponseBody,
6+
GetProjectsResponseBody,
7+
} from "@trigger.dev/core/v3";
8+
import { z } from "zod";
9+
import { prisma } from "~/db.server";
10+
import { createProject } from "~/models/project.server";
11+
import { logger } from "~/services/logger.server";
12+
import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server";
13+
import { isCuid } from "cuid";
14+
15+
const ParamsSchema = z.object({
16+
orgParam: z.string(),
17+
});
18+
19+
export async function loader({ request, params }: LoaderFunctionArgs) {
20+
logger.info("get projects", { url: request.url });
21+
22+
const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request);
23+
24+
if (!authenticationResult) {
25+
return json({ error: "Invalid or Missing Access Token" }, { status: 401 });
26+
}
27+
28+
const { orgParam } = ParamsSchema.parse(params);
29+
30+
const projects = await prisma.project.findMany({
31+
where: {
32+
organization: {
33+
...orgParamWhereClause(orgParam),
34+
deletedAt: null,
35+
members: {
36+
some: {
37+
userId: authenticationResult.userId,
38+
},
39+
},
40+
},
41+
version: "V3",
42+
deletedAt: null,
43+
},
44+
include: {
45+
organization: true,
46+
},
47+
});
48+
49+
if (!projects) {
50+
return json({ error: "Projects not found" }, { status: 404 });
51+
}
52+
53+
const result: GetProjectsResponseBody = projects.map((project) => ({
54+
id: project.id,
55+
externalRef: project.externalRef,
56+
name: project.name,
57+
slug: project.slug,
58+
createdAt: project.createdAt,
59+
organization: {
60+
id: project.organization.id,
61+
title: project.organization.title,
62+
slug: project.organization.slug,
63+
createdAt: project.organization.createdAt,
64+
},
65+
}));
66+
67+
return json(result);
68+
}
69+
70+
export async function action({ request, params }: ActionFunctionArgs) {
71+
const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request);
72+
73+
if (!authenticationResult) {
74+
return json({ error: "Invalid or Missing Access Token" }, { status: 401 });
75+
}
76+
77+
const { orgParam } = ParamsSchema.parse(params);
78+
79+
const organization = await prisma.organization.findFirst({
80+
where: {
81+
...orgParamWhereClause(orgParam),
82+
deletedAt: null,
83+
members: {
84+
some: {
85+
userId: authenticationResult.userId,
86+
},
87+
},
88+
},
89+
});
90+
91+
if (!organization) {
92+
return json({ error: "Organization not found" }, { status: 404 });
93+
}
94+
95+
const body = await request.json();
96+
const parsedBody = CreateProjectRequestBody.safeParse(body);
97+
98+
if (!parsedBody.success) {
99+
return json({ error: "Invalid request body" }, { status: 400 });
100+
}
101+
102+
const project = await createProject({
103+
organizationSlug: organization.slug,
104+
name: parsedBody.data.name,
105+
userId: authenticationResult.userId,
106+
version: "v3",
107+
});
108+
109+
const result: GetProjectResponseBody = {
110+
id: project.id,
111+
externalRef: project.externalRef,
112+
name: project.name,
113+
slug: project.slug,
114+
createdAt: project.createdAt,
115+
organization: {
116+
id: project.organization.id,
117+
title: project.organization.title,
118+
slug: project.organization.slug,
119+
createdAt: project.organization.createdAt,
120+
},
121+
};
122+
123+
return json(result);
124+
}
125+
126+
function orgParamWhereClause(orgParam: string) {
127+
// If the orgParam is an ID, or if it's a slug
128+
// IDs are cuid
129+
if (isCuid(orgParam)) {
130+
return {
131+
id: orgParam,
132+
};
133+
}
134+
135+
return {
136+
slug: orgParam,
137+
};
138+
}

0 commit comments

Comments
 (0)