Skip to content

Commit 5c0158e

Browse files
committed
Expose a basic version of the MCP server as an option for the dev CLI command
This just adds the main components for the MCP server. It hooks on the existing `dev` command and can be enabled by passing the `--mcp` flag. Currently only the `trigger-task` tool is exposed, which enables users trigger tasks via MCP and see the resulting run. Still WIP :)
1 parent de4ee19 commit 5c0158e

6 files changed

Lines changed: 554 additions & 7 deletions

File tree

packages/cli-v3/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"@types/eventsource": "^1.1.15",
5050
"@types/gradient-string": "^1.1.2",
5151
"@types/object-hash": "3.0.6",
52+
"@types/polka": "^0.5.7",
5253
"@types/react": "^18.2.48",
5354
"@types/resolve": "^1.20.6",
5455
"@types/rimraf": "^4.0.5",
@@ -75,6 +76,7 @@
7576
"dependencies": {
7677
"@clack/prompts": "^0.10.0",
7778
"@depot/cli": "0.0.1-cli.2.80.0",
79+
"@modelcontextprotocol/sdk": "^1.6.1",
7880
"@opentelemetry/api": "1.9.0",
7981
"@opentelemetry/api-logs": "0.52.1",
8082
"@opentelemetry/exporter-logs-otlp-http": "0.52.1",
@@ -114,6 +116,7 @@
114116
"p-retry": "^6.1.0",
115117
"partysocket": "^1.0.2",
116118
"pkg-types": "^1.1.3",
119+
"polka": "^0.5.2",
117120
"resolve": "^1.22.8",
118121
"semver": "^7.5.0",
119122
"signal-exit": "^4.1.0",

packages/cli-v3/src/apiClient.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
DevDequeueRequestBody,
3232
DevDequeueResponseBody,
3333
PromoteDeploymentResponseBody,
34+
ListRunResponse,
3435
} from "@trigger.dev/core/v3";
3536
import { zodfetch, zodfetchSSE, ApiError } from "@trigger.dev/core/v3/zodfetch";
3637
import { logger } from "./utilities/logger.js";
@@ -48,6 +49,7 @@ import {
4849
export class CliApiClient {
4950
constructor(
5051
public readonly apiURL: string,
52+
// TODO: consider making this required
5153
public readonly accessToken?: string
5254
) {
5355
this.apiURL = apiURL.replace(/\/$/, "");

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const DevCommandOptions = CommonCommandOptions.extend({
2020
envFile: z.string().optional(),
2121
keepTmpFiles: z.boolean().default(false),
2222
maxConcurrentRuns: z.coerce.number().optional(),
23+
mcp: z.boolean().default(false),
2324
});
2425

2526
export type DevCommandOptions = z.infer<typeof DevCommandOptions>;
@@ -48,6 +49,8 @@ export function configureDevCommand(program: Command) {
4849
"--keep-tmp-files",
4950
"Keep temporary files after the dev session ends, helpful for debugging"
5051
)
52+
// TODO: add a more detailed description, maybe a pointer to the docs on how to use MCP
53+
.option("--mcp", "Run an MCP server")
5154
).action(async (options) => {
5255
wrapCommandAction("dev", DevCommandOptions, options, async (opts) => {
5356
await devCommand(opts);

packages/cli-v3/src/dev/devSession.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { logger } from "../utilities/logger.js";
2323
import { clearTmpDirs, EphemeralDirectory, getTmpDir } from "../utilities/tempDirectories.js";
2424
import { startDevOutput } from "./devOutput.js";
2525
import { startWorkerRuntime } from "./devSupervisor.js";
26+
import { startMcpServer } from "./mcpServer.js";
2627

2728
export type DevSessionOptions = {
2829
name: string | undefined;
@@ -59,6 +60,16 @@ export async function startDevSession({
5960
dashboardUrl,
6061
});
6162

63+
if (rawArgs.mcp) {
64+
await startMcpServer({
65+
cliApiClient: client,
66+
devSession: {
67+
dashboardUrl,
68+
projectRef: rawConfig.project,
69+
},
70+
});
71+
}
72+
6273
const stopOutput = startDevOutput({
6374
name,
6475
dashboardUrl,
@@ -190,6 +201,7 @@ export async function startDevSession({
190201
stopBundling?.().catch((error) => {});
191202
runtime.shutdown().catch((error) => {});
192203
stopOutput();
204+
// TODO: stop MCP server
193205
},
194206
};
195207
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
3+
import { z } from "zod";
4+
import { logger } from "../utilities/logger.js";
5+
import { CliApiClient } from "../apiClient.js";
6+
import { ApiClient } from "@trigger.dev/core/v3";
7+
import polka from "polka";
8+
9+
let projectRef: string;
10+
let dashboardUrl: string;
11+
// there is some overlap between `ApiClient` and `CliApiClient` which is not ideal
12+
// we can address in this in the future, but for now we need keep using both
13+
// as `ApiClient` exposes most of the methods needed for the MCP tools
14+
let sdkApiClient: ApiClient;
15+
16+
let mcpTransport: SSEServerTransport | null = null;
17+
18+
const server = new McpServer({
19+
name: "trigger.dev",
20+
version: "1.0.0",
21+
});
22+
23+
server.tool(
24+
"trigger-task",
25+
"Trigger a task",
26+
{
27+
id: z.string().describe("The id of the task to trigger"),
28+
},
29+
async ({ id }) => {
30+
const result = await sdkApiClient.triggerTask(id, {
31+
// TODO: enable the user to pass in a custom payload
32+
payload: {},
33+
});
34+
35+
const taskRunUrl = `${dashboardUrl}/projects/v3/${projectRef}/runs/${result.id}`;
36+
37+
return {
38+
content: [
39+
{
40+
type: "text",
41+
text: JSON.stringify({ ...result, taskRunUrl }, null, 2),
42+
},
43+
],
44+
};
45+
}
46+
);
47+
48+
const app = polka();
49+
app.get("/sse", (_req, res) => {
50+
mcpTransport = new SSEServerTransport("/messages", res);
51+
server.connect(mcpTransport);
52+
});
53+
app.post("/messages", (req, res) => {
54+
if (mcpTransport) {
55+
mcpTransport.handlePostMessage(req, res);
56+
}
57+
});
58+
59+
export const startMcpServer = async (options: {
60+
cliApiClient: CliApiClient;
61+
devSession: {
62+
dashboardUrl: string;
63+
projectRef: string;
64+
};
65+
}) => {
66+
const { apiURL, accessToken } = options.cliApiClient;
67+
68+
if (!accessToken) {
69+
logger.error("No access token found in the API client, failed to start the MCP server");
70+
return;
71+
}
72+
73+
sdkApiClient = new ApiClient(apiURL, accessToken);
74+
projectRef = options.devSession.projectRef;
75+
dashboardUrl = options.devSession.dashboardUrl;
76+
77+
// TODO: make the port configurable
78+
const port = 3333;
79+
app.listen(port, () => {
80+
logger.info(`Trigger.dev MCP Server is now running on port ${port} ✨`);
81+
});
82+
};

0 commit comments

Comments
 (0)