diff --git a/main.ts b/main.ts index 4eb7ea1..cd79f3c 100644 --- a/main.ts +++ b/main.ts @@ -226,7 +226,7 @@ function parsePayload(inputs: Inputs): Payload { function setupMcpServers(ctx: MainContext): void { const allModes = [...modes, ...(ctx.payload.modes || [])]; - ctx.mcpServers = createMcpConfigs(ctx.githubInstallationToken, allModes); + ctx.mcpServers = createMcpConfigs(ctx.githubInstallationToken, allModes, ctx.payload); log.debug(`📋 MCP Config: ${JSON.stringify(ctx.mcpServers, null, 2)}`); } diff --git a/mcp/comment.ts b/mcp/comment.ts index 5e1632b..6ef5c3d 100644 --- a/mcp/comment.ts +++ b/mcp/comment.ts @@ -1,6 +1,39 @@ import { type } from "arktype"; +import type { Payload } from "../external.ts"; +import { agentsManifest } from "../external.ts"; +import { parseRepoContext } from "../utils/github.ts"; import { contextualize, tool } from "./shared.ts"; +function buildCommentFooter(payload: Payload): string { + const repoContext = parseRepoContext(); + const runId = process.env.GITHUB_RUN_ID; + + const agentName = payload.agent; + const agentInfo = agentName ? agentsManifest[agentName] : null; + const agentDisplayName = agentInfo?.displayName || "Unknown Agent"; + + // agent URLs based on manifest + const agentUrls: Record = { + claude: "https://claude.com/claude-code", + codex: "https://platform.openai.com/docs/guides/codex", + cursor: "https://cursor.com/", + gemini: "https://ai.google.dev/gemini-api/docs", + }; + + const agentUrl = agentName + ? agentUrls[agentName] || "https://pullfrog.ai" + : "https://pullfrog.ai"; + + // build workflow run URL + const workflowRunUrl = runId + ? `https://github.com/${repoContext.owner}/${repoContext.name}/actions/runs/${runId}` + : `https://github.com/${repoContext.owner}/${repoContext.name}`; + + return `--- + + Triggered by [Pullfrog](https://pullfrog.ai) | Using [${agentDisplayName}](${agentUrl}) | [View workflow run](${workflowRunUrl}) | [![Share on X](https://img.shields.io/badge/X-share-black)](https://x.com/pullfrogai)`; +} + export const Comment = type({ issueNumber: type.number.describe("the issue number to comment on"), body: type.string.describe("the comment body content"), @@ -73,11 +106,14 @@ export const CreateWorkingCommentTool = tool({ throw new Error("create_working_comment may not be called multiple times"); } + const footer = buildCommentFooter(ctx.payload); + const body = `${intent} ${footer}`; + const result = await ctx.octokit.rest.issues.createComment({ owner: ctx.owner, repo: ctx.name, issue_number: issueNumber, - body: `${intent} `, + body, }); workingCommentId = result.data.id; @@ -104,11 +140,14 @@ export const UpdateWorkingCommentTool = tool({ throw new Error("create_working_comment must be called before update_working_comment"); } + const footer = buildCommentFooter(ctx.payload); + const bodyWithFooter = `${body}${footer}`; + const result = await ctx.octokit.rest.issues.updateComment({ owner: ctx.owner, repo: ctx.name, comment_id: workingCommentId, - body, + body: bodyWithFooter, }); return { diff --git a/mcp/config.ts b/mcp/config.ts index a26e184..c374543 100644 --- a/mcp/config.ts +++ b/mcp/config.ts @@ -4,6 +4,7 @@ import type { McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk"; import { fromHere } from "@ark/fs"; +import type { Payload } from "../external.ts"; import { ghPullfrogMcpName } from "../external.ts"; import type { Mode } from "../modes.ts"; import { parseRepoContext } from "../utils/github.ts"; @@ -12,7 +13,11 @@ export type McpName = typeof ghPullfrogMcpName; export type McpConfigs = Record; -export function createMcpConfigs(githubInstallationToken: string, modes: Mode[]): McpConfigs { +export function createMcpConfigs( + githubInstallationToken: string, + modes: Mode[], + payload: Payload +): McpConfigs { const repoContext = parseRepoContext(); const githubRepository = `${repoContext.owner}/${repoContext.name}`; @@ -20,15 +25,23 @@ export function createMcpConfigs(githubInstallationToken: string, modes: Mode[]) // In development, server.ts is in the same directory as this file (config.ts) const serverPath = process.env.GITHUB_ACTIONS ? fromHere("mcp-server") : fromHere("server.ts"); + const env: Record = { + GITHUB_INSTALLATION_TOKEN: githubInstallationToken, + GITHUB_REPOSITORY: githubRepository, + PULLFROG_MODES: JSON.stringify(modes), + PULLFROG_PAYLOAD: JSON.stringify(payload), + }; + + // pass through GITHUB_RUN_ID if available (automatically set in GitHub Actions) + if (process.env.GITHUB_RUN_ID) { + env.GITHUB_RUN_ID = process.env.GITHUB_RUN_ID; + } + return { [ghPullfrogMcpName]: { command: "node", args: [serverPath], - env: { - GITHUB_INSTALLATION_TOKEN: githubInstallationToken, - GITHUB_REPOSITORY: githubRepository, - PULLFROG_MODES: JSON.stringify(modes), - }, + env, }, }; } diff --git a/mcp/shared.ts b/mcp/shared.ts index d86490d..6a70251 100644 --- a/mcp/shared.ts +++ b/mcp/shared.ts @@ -1,7 +1,7 @@ -import { cached } from "@ark/util"; import { Octokit } from "@octokit/rest"; import type { StandardSchemaV1 } from "@standard-schema/spec"; import type { FastMCP, Tool } from "fastmcp"; +import type { Payload } from "../external.ts"; import { log } from "../utils/cli.ts"; import { parseRepoContext, type RepoContext } from "../utils/github.ts"; @@ -13,7 +13,22 @@ export interface ToolResult { isError?: boolean; } -export const getMcpContext = cached((): McpContext => { +export function getPayload(): Payload { + const payloadEnv = process.env.PULLFROG_PAYLOAD; + if (!payloadEnv) { + throw new Error("PULLFROG_PAYLOAD environment variable is required"); + } + + try { + return JSON.parse(payloadEnv) as Payload; + } catch (error) { + throw new Error( + `Failed to parse PULLFROG_PAYLOAD: ${error instanceof Error ? error.message : String(error)}` + ); + } +} + +export function getMcpContext(): McpContext { const githubInstallationToken = process.env.GITHUB_INSTALLATION_TOKEN; if (!githubInstallationToken) { throw new Error("GITHUB_INSTALLATION_TOKEN environment variable is required"); @@ -24,11 +39,13 @@ export const getMcpContext = cached((): McpContext => { octokit: new Octokit({ auth: githubInstallationToken, }), + payload: getPayload(), }; -}); +} export interface McpContext extends RepoContext { octokit: Octokit; + payload: Payload; } export const tool = (toolDef: Tool>) => {