Skip to content
Merged
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
fa3bdc5
Initial scaffolding
ericallam Aug 6, 2025
c7674cc
MCP auth and list_projects tool
ericallam Aug 6, 2025
4c03033
Add a few more tools
ericallam Aug 6, 2025
832707d
Remove the .mcp.log file
ericallam Aug 6, 2025
8e4d680
Ignore .mcp.log
ericallam Aug 6, 2025
e4a7314
Adding initialize project tool
ericallam Aug 7, 2025
43ace92
Add vscode installation
ericallam Aug 7, 2025
a240b30
Adding getTasks and triggerTask tools
ericallam Aug 7, 2025
0197742
Add dev connection message
ericallam Aug 7, 2025
3c21be3
Restructure transport
ericallam Aug 8, 2025
045c092
Add the install-mcp command
ericallam Aug 8, 2025
1f86fa6
Added cline support
ericallam Aug 9, 2025
268a778
Amp support
ericallam Aug 9, 2025
eaa1578
Added getRunDetails tools
ericallam Aug 9, 2025
d33aaca
Background worker -> Local worker
ericallam Aug 10, 2025
166a254
Add install mcp prompt to init and dev, and add support for codex-cli…
ericallam Aug 11, 2025
d62886c
Add Zed support
ericallam Aug 11, 2025
181acff
Deploying with MCP
ericallam Aug 11, 2025
a47ed15
opencode support
ericallam Aug 11, 2025
aa2dae3
Add list runs tool
ericallam Aug 12, 2025
e0f1e17
Add support for getting the trace of a run in get_run_details
ericallam Aug 12, 2025
b3bb61f
Add cancel_run tool
ericallam Aug 12, 2025
8bcf320
Add list deploys tool
ericallam Aug 12, 2025
361e2bb
Support for preview branches
ericallam Aug 12, 2025
088c3a9
Improve the MCP authentication message
ericallam Aug 13, 2025
ee1725f
fix asking about installing the MCP server after already answering
ericallam Aug 13, 2025
ca993fe
fix overriding all of settings
ericallam Aug 17, 2025
d45ad5d
Massive MCP cleanup
ericallam Aug 17, 2025
98e7c66
a couple of coderabbit fixes
ericallam Aug 18, 2025
4100a9e
Improve date parsing from search query
ericallam Aug 18, 2025
21639e0
more coderabbit
ericallam Aug 18, 2025
a8ca773
fix error message
ericallam Aug 18, 2025
d4e78d7
Improve the deploy experience
ericallam Aug 18, 2025
03aec5a
Add project redirect for /projects/$ref
ericallam Aug 18, 2025
599f6a2
Improved the get tasks tool, fixed mintlify MCP search
ericallam Aug 18, 2025
f60056e
Much better get run details tool output with more detailed trace data
ericallam Aug 18, 2025
62a08b4
format the runs list
ericallam Aug 18, 2025
6b8704b
add install-rules command
ericallam Aug 19, 2025
8f34277
make updating rules files idempotent
ericallam Aug 19, 2025
08cd20a
clients -> targets in the install-rules command
ericallam Aug 20, 2025
8bb9f31
Add the install rules wizard to the mcp install command
ericallam Aug 20, 2025
1efecdd
add rules
ericallam Aug 20, 2025
f6c5aa2
Add changeset
ericallam Aug 20, 2025
220c610
Some tweaks to the rules and how they are installed
ericallam Aug 20, 2025
f25e90a
various coderabbit fixes
ericallam Aug 20, 2025
cb1ff21
fix PR review issues
ericallam Aug 20, 2025
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
8 changes: 2 additions & 6 deletions .cursor/mcp.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
{
"mcpServers": {
"trigger.dev": {
"url": "http://localhost:3333/sse"
}
}
}
"mcpServers": {}
}
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,5 @@ apps/**/public/build
/packages/core/src/package.json
/packages/trigger-sdk/src/package.json
/packages/python/src/package.json
.claude
.claude
.mcp.log
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
import { CheckCircleIcon } from "@heroicons/react/24/solid";
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { title } from "process";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { z } from "zod";
import { ErrorIcon } from "~/assets/icons/ErrorIcon";
import { AppContainer, MainCenteredContainer } from "~/components/layout/AppLayout";
import { LinkButton } from "~/components/primitives/Buttons";
import { Callout } from "~/components/primitives/Callout";
import { Header1 } from "~/components/primitives/Headers";
import { Icon } from "~/components/primitives/Icon";
import { Paragraph } from "~/components/primitives/Paragraph";
import { logger } from "~/services/logger.server";
import { createPersonalAccessTokenFromAuthorizationCode } from "~/services/personalAccessToken.server";
import { requireUserId } from "~/services/session.server";
import { rootPath } from "~/utils/pathBuilder";

const ParamsSchema = z.object({
authorizationCode: z.string(),
});

const SearchParamsSchema = z.object({
source: z.string().optional(),
clientName: z.string().optional(),
});

export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const userId = await requireUserId(request);

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

const url = new URL(request.url);
const searchObject = Object.fromEntries(url.searchParams.entries());

const searchParams = SearchParamsSchema.safeParse(searchObject);

const source = (searchParams.success ? searchParams.data.source : undefined) ?? "cli";
const clientName = (searchParams.success ? searchParams.data.clientName : undefined) ?? "unknown";

try {
const personalAccessToken = await createPersonalAccessTokenFromAuthorizationCode(
parsedParams.data.authorizationCode,
userId
);
return typedjson({
success: true as const,
source,
clientName,
});
} catch (error) {
if (error instanceof Response) {
Expand All @@ -49,6 +60,8 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
return typedjson({
success: false as const,
error: error.message,
source,
clientName,
});
}

Expand All @@ -73,7 +86,7 @@ export default function Page() {
<Icon icon={CheckCircleIcon} className="h-6 w-6 text-emerald-500" /> Successfully
authenticated
</Header1>
<Paragraph>Return to your terminal to continue.</Paragraph>
<Paragraph>{getInstructionsForSource(result.source, result.clientName)}</Paragraph>
</div>
) : (
<div>
Expand All @@ -91,3 +104,21 @@ export default function Page() {
</AppContainer>
);
}

const prettyClientNames: Record<string, string> = {
"claude-code": "Claude Code",
"cursor-vscode": "Cursor",
"Visual Studio Code": "VSCode",
"windsurf-client": "Windsurf",
"claude-ai": "Claude Desktop",
};

function getInstructionsForSource(source: string, clientName: string) {
if (source === "mcp") {
if (clientName) {
return `Return to your ${prettyClientNames[clientName] ?? clientName} to continue.`;
}
}

return `Return to your terminal to continue.`;
}
119 changes: 119 additions & 0 deletions apps/webapp/app/routes/api.v1.deployments.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { ActionFunctionArgs, json } from "@remix-run/server-runtime";
import {
ApiDeploymentListSearchParams,
InitializeDeploymentRequestBody,
InitializeDeploymentResponseBody,
} from "@trigger.dev/core/v3";
import { $replica } from "~/db.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server";
import { ServiceValidationError } from "~/v3/services/baseService.server";
import { InitializeDeploymentService } from "~/v3/services/initializeDeployment.server";

Expand Down Expand Up @@ -60,3 +63,119 @@ export async function action({ request, params }: ActionFunctionArgs) {
}
}
}

export const loader = createLoaderApiRoute(
{
searchParams: ApiDeploymentListSearchParams,
allowJWT: true,
corsStrategy: "none",
authorization: {
action: "read",
resource: () => ({ deployments: "list" }),
superScopes: ["read:deployments", "read:all", "admin"],
},
findResource: async () => 1, // This is a dummy function, we don't need to find a resource
},
async ({ searchParams, authentication }) => {
const limit = Math.max(Math.min(searchParams["page[size]"] ?? 20, 100), 5);

const afterDeployment = searchParams["page[after]"]
? await $replica.workerDeployment.findFirst({
where: {
friendlyId: searchParams["page[after]"],
environmentId: authentication.environment.id,
},
})
: undefined;

const deployments = await $replica.workerDeployment.findMany({
where: {
environmentId: authentication.environment.id,
...(afterDeployment ? { id: { lt: afterDeployment.id } } : {}),
...getCreatedAtFilter(searchParams),
...(searchParams.status ? { status: searchParams.status } : {}),
},
orderBy: {
id: "desc",
},
take: limit + 1,
});

const hasMore = deployments.length > limit;
const nextCursor = hasMore ? deployments[limit - 1].friendlyId : undefined;
const data = hasMore ? deployments.slice(0, limit) : deployments;

return json({
data: data.map((deployment) => ({
id: deployment.friendlyId,
createdAt: deployment.createdAt,
shortCode: deployment.shortCode,
version: deployment.version.toString(),
runtime: deployment.runtime,
runtimeVersion: deployment.runtimeVersion,
status: deployment.status,
deployedAt: deployment.deployedAt,
git: deployment.git,
error: deployment.errorData ?? undefined,
})),
pagination: {
next: nextCursor,
},
});
}
);

import parseDuration from "parse-duration";
import { parseDate } from "@trigger.dev/core/v3/isomorphic";

function getCreatedAtFilter(searchParams: ApiDeploymentListSearchParams) {
if (searchParams.period) {
const duration = parseDuration(searchParams.period, "ms");

if (!duration) {
throw new ServiceValidationError(
`Invalid search query parameter: period=${searchParams.period}`,
400
);
}

return {
createdAt: {
gte: new Date(Date.now() - duration),
lte: new Date(),
},
};
}

if (searchParams.from && searchParams.to) {
const fromDate = safeDateFromString(searchParams.from, "from");
const toDate = safeDateFromString(searchParams.to, "to");

return {
createdAt: {
gte: fromDate,
lte: toDate,
},
};
}

if (searchParams.from) {
const fromDate = safeDateFromString(searchParams.from, "from");
return {
createdAt: {
gte: fromDate,
},
};
}

return {};
}

function safeDateFromString(value: string, paramName: string) {
const date = parseDate(value);

if (!date) {
throw new ServiceValidationError(`Invalid search query parameter: ${paramName}=${value}`, 400);
}
return date;
}
138 changes: 138 additions & 0 deletions apps/webapp/app/routes/api.v1.orgs.$orgParam.projects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import {
CreateProjectRequestBody,
GetProjectResponseBody,
GetProjectsResponseBody,
} from "@trigger.dev/core/v3";
import { z } from "zod";
import { prisma } from "~/db.server";
import { createProject } from "~/models/project.server";
import { logger } from "~/services/logger.server";
import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server";
import { isCuid } from "cuid";

const ParamsSchema = z.object({
orgParam: z.string(),
});

export async function loader({ request, params }: LoaderFunctionArgs) {
logger.info("get projects", { url: request.url });

const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request);

if (!authenticationResult) {
return json({ error: "Invalid or Missing Access Token" }, { status: 401 });
}

const { orgParam } = ParamsSchema.parse(params);

const projects = await prisma.project.findMany({
where: {
organization: {
...orgParamWhereClause(orgParam),
deletedAt: null,
members: {
some: {
userId: authenticationResult.userId,
},
},
},
version: "V3",
deletedAt: null,
},
include: {
organization: true,
},
});

if (!projects) {
return json({ error: "Projects not found" }, { status: 404 });
}

const result: GetProjectsResponseBody = projects.map((project) => ({
id: project.id,
externalRef: project.externalRef,
name: project.name,
slug: project.slug,
createdAt: project.createdAt,
organization: {
id: project.organization.id,
title: project.organization.title,
slug: project.organization.slug,
createdAt: project.organization.createdAt,
},
}));

return json(result);
}

export async function action({ request, params }: ActionFunctionArgs) {
const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request);

if (!authenticationResult) {
return json({ error: "Invalid or Missing Access Token" }, { status: 401 });
}

const { orgParam } = ParamsSchema.parse(params);

const organization = await prisma.organization.findFirst({
where: {
...orgParamWhereClause(orgParam),
deletedAt: null,
members: {
some: {
userId: authenticationResult.userId,
},
},
},
});

if (!organization) {
return json({ error: "Organization not found" }, { status: 404 });
}

const body = await request.json();
const parsedBody = CreateProjectRequestBody.safeParse(body);

if (!parsedBody.success) {
return json({ error: "Invalid request body" }, { status: 400 });
}

const project = await createProject({
organizationSlug: organization.slug,
name: parsedBody.data.name,
userId: authenticationResult.userId,
version: "v3",
});

const result: GetProjectResponseBody = {
id: project.id,
externalRef: project.externalRef,
name: project.name,
slug: project.slug,
createdAt: project.createdAt,
organization: {
id: project.organization.id,
title: project.organization.title,
slug: project.organization.slug,
createdAt: project.organization.createdAt,
},
};

return json(result);
}

function orgParamWhereClause(orgParam: string) {
// If the orgParam is an ID, or if it's a slug
// IDs are cuid
if (isCuid(orgParam)) {
return {
id: orgParam,
};
}

return {
slug: orgParam,
};
}
Loading
Loading