UNPKG

trigger.dev

Version:

A Command-Line Interface for Trigger.dev projects

301 lines 13 kB
import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { toolsMetadata } from "../config.js"; import { formatRun, formatRunList, formatRunShape, formatRunTrace, formatSpanDetail, } from "../formatters.js"; import { CommonRunsInput, GetRunDetailsInput, GetSpanDetailsInput, ListRunsInput, WaitForRunInput, } from "../schemas.js"; import { respondWithError, toolHandler } from "../utils.js"; // Cache formatted traces in temp files keyed by runId. // Each entry stores the file path and total line count. const traceCache = new Map(); const TRACE_CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes function getTraceCacheDir() { const dir = path.join(os.tmpdir(), "trigger-mcp-traces"); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } return dir; } /** Read a page of lines from a file. Returns the lines and whether there are more. */ function readLinesPage(filePath, offset, limit, totalLines) { const content = fs.readFileSync(filePath, "utf-8"); const allLines = content.split("\n"); const pageLines = allLines.slice(offset, offset + limit); const end = offset + limit; const hasMore = end < totalLines; return { lines: pageLines, hasMore, nextCursor: hasMore ? String(end) : null, }; } export const getRunDetailsTool = { name: toolsMetadata.get_run_details.name, title: toolsMetadata.get_run_details.title, description: toolsMetadata.get_run_details.description, inputSchema: GetRunDetailsInput.shape, handler: toolHandler(GetRunDetailsInput.shape, async (input, { ctx }) => { ctx.logger?.log("calling get_run_details", { input }); if (ctx.options.devOnly && input.environment !== "dev") { return respondWithError(`This MCP server is only available for the dev environment. You tried to access the ${input.environment} environment. Remove the --dev-only flag to access other environments.`); } const projectRef = await ctx.getProjectRef({ projectRef: input.projectRef, cwd: input.configPath, }); const apiClient = await ctx.getApiClient({ projectRef, environment: input.environment, scopes: [`read:runs:${input.runId}`], branch: input.branch, }); const limit = input.maxTraceLines; const offset = input.cursor ? parseInt(input.cursor, 10) : 0; // Check if we have a cached trace file for this run let cached = traceCache.get(input.runId); if (cached && Date.now() >= cached.expiresAt) { // Expired — clean up try { fs.unlinkSync(cached.filePath); } catch { } traceCache.delete(input.runId); cached = undefined; } let formattedRun; let runUrl; if (!cached) { // Fetch and cache the full trace const [runResult, traceResult] = await Promise.all([ apiClient.retrieveRun(input.runId), apiClient.retrieveRunTrace(input.runId), ]); formattedRun = formatRun(runResult); runUrl = await ctx.getDashboardUrl(`/projects/v3/${projectRef}/runs/${runResult.id}`); // Format the full trace (no line limit — we're writing to a file) const fullTrace = formatRunTrace(traceResult.trace, Infinity); const traceLines = fullTrace.split("\n"); // Write to temp file const filePath = path.join(getTraceCacheDir(), `${input.runId}.txt`); fs.writeFileSync(filePath, fullTrace, "utf-8"); // Only cache runs in terminal states — active runs need fresh traces const terminalStatuses = new Set([ "COMPLETED", "CANCELED", "FAILED", "CRASHED", "SYSTEM_FAILURE", "EXPIRED", "TIMED_OUT", ]); cached = { filePath, totalLines: traceLines.length, expiresAt: Date.now() + TRACE_CACHE_TTL_MS, }; if (terminalStatuses.has(runResult.status)) { traceCache.set(input.runId, cached); } } // Read the requested page const page = readLinesPage(cached.filePath, offset, limit, cached.totalLines); const content = []; // Only include run details on the first page if (offset === 0) { if (!formattedRun) { // Cursor pagination — fetch run details for context const runResult = await apiClient.retrieveRun(input.runId); formattedRun = formatRun(runResult); runUrl = await ctx.getDashboardUrl(`/projects/v3/${projectRef}/runs/${runResult.id}`); } content.push("## Run Details"); content.push(formattedRun); content.push(""); } content.push(`## Run Trace (lines ${offset + 1}-${offset + page.lines.length} of ${cached.totalLines})`); content.push(page.lines.join("\n")); if (page.hasMore) { content.push(""); content.push(`**More trace available.** Call \`get_run_details\` again with \`cursor: "${page.nextCursor}"\` and \`runId: "${input.runId}"\` to see the next page.`); } if (runUrl && offset === 0) { content.push(""); content.push(`[View in dashboard](${runUrl})`); } return { content: [ { type: "text", text: content.join("\n"), }, ], }; }), }; export const getSpanDetailsTool = { name: toolsMetadata.get_span_details.name, title: toolsMetadata.get_span_details.title, description: toolsMetadata.get_span_details.description, inputSchema: GetSpanDetailsInput.shape, handler: toolHandler(GetSpanDetailsInput.shape, async (input, { ctx }) => { ctx.logger?.log("calling get_span_details", { input }); if (ctx.options.devOnly && input.environment !== "dev") { return respondWithError(`This MCP server is only available for the dev environment. You tried to access the ${input.environment} environment. Remove the --dev-only flag to access other environments.`); } const projectRef = await ctx.getProjectRef({ projectRef: input.projectRef, cwd: input.configPath, }); const apiClient = await ctx.getApiClient({ projectRef, environment: input.environment, scopes: [`read:runs:${input.runId}`], branch: input.branch, }); const spanDetail = await apiClient.retrieveSpan(input.runId, input.spanId); const formatted = formatSpanDetail(spanDetail); const runUrl = await ctx.getDashboardUrl(`/projects/v3/${projectRef}/runs/${input.runId}`); const content = [formatted]; if (runUrl) { content.push(""); content.push(`[View run in dashboard](${runUrl})`); } return { content: [{ type: "text", text: content.join("\n") }], }; }), }; export const waitForRunToCompleteTool = { name: toolsMetadata.wait_for_run_to_complete.name, title: toolsMetadata.wait_for_run_to_complete.title, description: toolsMetadata.wait_for_run_to_complete.description, inputSchema: WaitForRunInput.shape, handler: toolHandler(WaitForRunInput.shape, async (input, { ctx, signal }) => { ctx.logger?.log("calling wait_for_run_to_complete", { input }); if (ctx.options.devOnly && input.environment !== "dev") { return respondWithError(`This MCP server is only available for the dev environment. You tried to access the ${input.environment} environment. Remove the --dev-only flag to access other environments.`); } const projectRef = await ctx.getProjectRef({ projectRef: input.projectRef, cwd: input.configPath, }); const apiClient = await ctx.getApiClient({ projectRef, environment: input.environment, scopes: [`read:runs:${input.runId}`], branch: input.branch, }); const timeoutMs = input.timeoutInSeconds * 1000; const timeoutSignal = AbortSignal.timeout(timeoutMs); const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal; const runSubscription = apiClient.subscribeToRun(input.runId, { signal: combinedSignal }); const readableStream = runSubscription.getReader(); let run = null; let timedOut = false; try { while (true) { const { done, value } = await readableStream.read(); if (done) { break; } run = value; if (value.isCompleted) { break; } } } catch (error) { if (timeoutSignal.aborted) { timedOut = true; } else { throw error; } } if (!run) { return respondWithError("Run not found"); } const prefix = timedOut ? `Timed out after ${input.timeoutInSeconds}s. Returning current run state:\n\n` : ""; return { content: [{ type: "text", text: prefix + formatRunShape(run) }], }; }), }; export const cancelRunTool = { name: toolsMetadata.cancel_run.name, title: toolsMetadata.cancel_run.title, description: toolsMetadata.cancel_run.description, inputSchema: CommonRunsInput.shape, handler: toolHandler(CommonRunsInput.shape, async (input, { ctx }) => { ctx.logger?.log("calling cancel_run", { input }); if (ctx.options.devOnly && input.environment !== "dev") { return respondWithError(`This MCP server is only available for the dev environment. You tried to access the ${input.environment} environment. Remove the --dev-only flag to access other environments.`); } const projectRef = await ctx.getProjectRef({ projectRef: input.projectRef, cwd: input.configPath, }); const apiClient = await ctx.getApiClient({ projectRef, environment: input.environment, scopes: [`write:runs:${input.runId}`, `read:runs:${input.runId}`], branch: input.branch, }); await apiClient.cancelRun(input.runId); const retrieveResult = await apiClient.retrieveRun(input.runId); const runUrl = await ctx.getDashboardUrl(`/projects/v3/${projectRef}/runs/${retrieveResult.id}`); const content = [ `Run ${retrieveResult.id} canceled.`, `Status: ${retrieveResult.status}`, `Task: ${retrieveResult.taskIdentifier}`, `[View in dashboard](${runUrl})`, ]; return { content: [{ type: "text", text: content.join("\n") }], }; }), }; export const listRunsTool = { name: toolsMetadata.list_runs.name, title: toolsMetadata.list_runs.title, description: toolsMetadata.list_runs.description, inputSchema: ListRunsInput.shape, handler: toolHandler(ListRunsInput.shape, async (input, { ctx }) => { ctx.logger?.log("calling list_runs", { input }); if (ctx.options.devOnly && input.environment !== "dev") { return respondWithError(`This MCP server is only available for the dev environment. You tried to access the ${input.environment} environment. Remove the --dev-only flag to access other environments.`); } const projectRef = await ctx.getProjectRef({ projectRef: input.projectRef, cwd: input.configPath, }); const apiClient = await ctx.getApiClient({ projectRef, environment: input.environment, scopes: ["read:runs"], branch: input.branch, }); const $from = typeof input.from === "string" ? new Date(input.from) : undefined; const $to = typeof input.to === "string" ? new Date(input.to) : undefined; const result = await apiClient.listRuns({ after: input.cursor, limit: input.limit, status: input.status, taskIdentifier: input.taskIdentifier, version: input.version, tag: input.tag, from: $from, to: $to, period: input.period, machine: input.machine, region: input.region, }); const formattedRuns = formatRunList(result); return { content: [{ type: "text", text: formattedRuns }], }; }), }; //# sourceMappingURL=runs.js.map