From c668578c6f0786a502d7f8b440160425985dc39e Mon Sep 17 00:00:00 2001 From: David Blass Date: Fri, 17 Oct 2025 22:26:24 -0400 Subject: [PATCH] refactor mcp and add instructions prefix --- agents/claude.ts | 61 ++++++++++++++----------------------------- agents/shared.ts | 6 +++++ agents/types.ts | 4 +-- fixtures/basic.txt | 4 +-- main.ts | 1 + mcp/comment.ts | 24 +++++------------ mcp/config.ts | 4 ++- mcp/server.ts | 2 +- mcp/shared.ts | 23 +++++++++++++++- package.json | 5 ++-- utils/github.ts | 31 ++++++++++++++++++---- utils/repo-context.ts | 22 ---------------- 12 files changed, 91 insertions(+), 96 deletions(-) create mode 100644 agents/shared.ts delete mode 100644 utils/repo-context.ts diff --git a/agents/claude.ts b/agents/claude.ts index b9db502..aef5a64 100644 --- a/agents/claude.ts +++ b/agents/claude.ts @@ -3,6 +3,7 @@ import * as core from "@actions/core"; import { createMcpConfig } from "../mcp/config.ts"; import { spawn } from "../utils/subprocess.ts"; import { boxString, tableString } from "../utils/table.ts"; +import { instructions } from "./shared.ts"; import type { Agent, AgentConfig, AgentResult } from "./types.ts"; /** @@ -10,7 +11,7 @@ import type { Agent, AgentConfig, AgentResult } from "./types.ts"; */ export class ClaudeAgent implements Agent { private apiKey: string; - private githubInstallationToken?: string; + private githubInstallationToken: string; public runStats = { toolsUsed: 0, turns: 0, @@ -18,9 +19,6 @@ export class ClaudeAgent implements Agent { }; constructor(config: AgentConfig) { - if (!config.apiKey) { - throw new Error("Claude agent requires an API key"); - } this.apiKey = config.apiKey; this.githubInstallationToken = config.githubInstallationToken; } @@ -51,10 +49,7 @@ export class ClaudeAgent implements Agent { try { const result = await spawn({ cmd: "bash", - args: [ - "-c", - "curl -fsSL https://claude.ai/install.sh | bash -s 1.0.93", - ], + args: ["-c", "curl -fsSL https://claude.ai/install.sh | bash -s 1.0.93"], env: { ANTHROPIC_API_KEY: this.apiKey }, timeout: 120000, // 2 minute timeout onStdout: () => {}, @@ -62,9 +57,7 @@ export class ClaudeAgent implements Agent { }); if (result.exitCode !== 0) { - throw new Error( - `Installation failed with exit code ${result.exitCode}: ${result.stderr}` - ); + throw new Error(`Installation failed with exit code ${result.exitCode}: ${result.stderr}`); } core.info("Claude Code installed successfully"); @@ -81,7 +74,16 @@ export class ClaudeAgent implements Agent { try { const claudePath = `${process.env.HOME}/.local/bin/claude`; + + const env = { + ANTHROPIC_API_KEY: this.apiKey, + }; + console.log(boxString(prompt, { title: "Prompt" })); + + const mcpConfig = createMcpConfig(this.githubInstallationToken); + console.log("📋 MCP Config:", mcpConfig); + const args = [ "--print", "--output-format", @@ -90,22 +92,10 @@ export class ClaudeAgent implements Agent { "--debug", "--permission-mode", "bypassPermissions", + "--mcp-config", + mcpConfig, ]; - if (!this.githubInstallationToken) { - throw new Error( - "GITHUB_INSTALLATION_TOKEN is required for GitHub integration" - ); - } - - const mcpConfig = createMcpConfig(this.githubInstallationToken); - console.log("📋 MCP Config:", mcpConfig); - args.push("--mcp-config", mcpConfig); - - const env = { - ANTHROPIC_API_KEY: this.apiKey, - }; - core.startGroup("🔄 Run details"); this.runStats = { @@ -121,7 +111,7 @@ export class ClaudeAgent implements Agent { cmd: claudePath, args, env, - input: prompt, + input: `${instructions} ${prompt}`, timeout: 10 * 60 * 1000, // 10 minutes onStdout: (_chunk) => { processJSONChunk(_chunk, this); @@ -161,8 +151,7 @@ export class ClaudeAgent implements Agent { try { core.endGroup(); } catch {} - const errorMessage = - error instanceof Error ? error.message : "Unknown error"; + const errorMessage = error instanceof Error ? error.message : "Unknown error"; return { success: false, error: `Failed to execute Claude Code: ${errorMessage}`, @@ -188,12 +177,7 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void { ["model", parsedChunk.model], ["cwd", parsedChunk.cwd], ["permission_mode", parsedChunk.permissionMode], - [ - "tools", - parsedChunk.tools?.length - ? `${parsedChunk.tools.length} tools` - : "none", - ], + ["tools", parsedChunk.tools?.length ? `${parsedChunk.tools.length} tools` : "none"], [ "mcp_servers", parsedChunk.mcp_servers?.length @@ -220,9 +204,7 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void { for (const content of parsedChunk.message.content) { if (content.type === "text") { if (content.text.trim()) { - core.info( - boxString(content.text.trim(), { title: "Claude Code" }) - ); + core.info(boxString(content.text.trim(), { title: "Claude Code" })); } } else if (content.type === "tool_use") { if (agent) { @@ -307,10 +289,7 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void { if (parsedChunk.subtype === "success") { core.info( tableString([ - [ - "Cost", - `$${parsedChunk.total_cost_usd?.toFixed(4) || "0.0000"}`, - ], + ["Cost", `$${parsedChunk.total_cost_usd?.toFixed(4) || "0.0000"}`], ["Input Tokens", parsedChunk.usage?.input_tokens || 0], ["Output Tokens", parsedChunk.usage?.output_tokens || 0], ["Duration", `${parsedChunk.duration_ms}ms`], diff --git a/agents/shared.ts b/agents/shared.ts new file mode 100644 index 0000000..572f8c0 --- /dev/null +++ b/agents/shared.ts @@ -0,0 +1,6 @@ +import { mcpServerName } from "../mcp/config.ts"; + +export const instructions = `- use the ${mcpServerName} MCP server to interact with github +- if ${mcpServerName} is not available or doesn't include the functionality you need, describe why and bail +- do not under any circumstances use the gh cli +`; diff --git a/agents/types.ts b/agents/types.ts index c33b33f..e963c9b 100644 --- a/agents/types.ts +++ b/agents/types.ts @@ -29,6 +29,6 @@ export interface AgentResult { * Configuration for agent creation */ export interface AgentConfig { - apiKey?: string; - [key: string]: any; + apiKey: string; + githubInstallationToken: string; } diff --git a/fixtures/basic.txt b/fixtures/basic.txt index 6147147..a554619 100644 --- a/fixtures/basic.txt +++ b/fixtures/basic.txt @@ -1,3 +1 @@ -Use the MCP GitHub comment tool to add a comment containing your best frog joke to GitHub issue https://github.com/pullfrogai/scratch/issues/2. - -Do not use the gh cli. If the mcp tool does not work, bail. \ No newline at end of file +add a comment containing your best frog joke to GitHub issue https://github.com/pullfrogai/scratch/issues/2 diff --git a/main.ts b/main.ts index 40133fb..21eb9b0 100644 --- a/main.ts +++ b/main.ts @@ -27,6 +27,7 @@ export async function main(inputs: Inputs): Promise { apiKey: inputs.anthropic_api_key!, githubInstallationToken, }); + await agent.install(); const result = await agent.execute(inputs.prompt); diff --git a/mcp/comment.ts b/mcp/comment.ts index a83787c..0d73af3 100644 --- a/mcp/comment.ts +++ b/mcp/comment.ts @@ -1,7 +1,5 @@ -import { Octokit } from "@octokit/rest"; import { type } from "arktype"; -import { resolveRepoContext } from "../utils/repo-context.ts"; -import { tool } from "./shared.ts"; +import { getMcpContext, tool } from "./shared.ts"; export const Comment = type({ issueNumber: type.number.describe("the issue number to comment on"), @@ -12,22 +10,12 @@ export const CommentTool = tool({ name: "create_issue_comment", description: "Create a comment on a GitHub issue", parameters: Comment, - execute: async ({ issueNumber, body }: { issueNumber: number; body: string }) => { + execute: async ({ issueNumber, body }) => { + const ctx = getMcpContext(); try { - const githubInstallationToken = process.env.GITHUB_INSTALLATION_TOKEN; - if (!githubInstallationToken) { - throw new Error("GITHUB_INSTALLATION_TOKEN environment variable is required"); - } - - const repoContext = resolveRepoContext(); - - const octokit = new Octokit({ - auth: githubInstallationToken, - }); - - const result = await octokit.rest.issues.createComment({ - owner: repoContext.owner, - repo: repoContext.name, + const result = await ctx.octokit.rest.issues.createComment({ + owner: ctx.owner, + repo: ctx.name, issue_number: issueNumber, body: body, }); diff --git a/mcp/config.ts b/mcp/config.ts index 9ecfdd9..c6ae74c 100644 --- a/mcp/config.ts +++ b/mcp/config.ts @@ -5,6 +5,8 @@ import { fromHere } from "@ark/fs"; const actionPath = fromHere(".."); +export const mcpServerName = "gh-pullfrog"; + export function createMcpConfig(githubInstallationToken: string) { const githubRepository = process.env.GITHUB_REPOSITORY; if (!githubRepository) { @@ -16,7 +18,7 @@ export function createMcpConfig(githubInstallationToken: string) { return JSON.stringify( { mcpServers: { - minimal_github_comment: { + [mcpServerName]: { command: "node", args: [`${actionPath}/mcp/server.ts`], env: { diff --git a/mcp/server.ts b/mcp/server.ts index 360f61c..9cc4306 100644 --- a/mcp/server.ts +++ b/mcp/server.ts @@ -5,7 +5,7 @@ import { CommentTool } from "./comment.ts"; import { addTools } from "./shared.ts"; const server = new FastMCP({ - name: "Minimal GitHub Issue Comment Server", + name: "gh-pullfrog", version: "0.0.1", }); diff --git a/mcp/shared.ts b/mcp/shared.ts index 59cfee9..ee1210e 100644 --- a/mcp/shared.ts +++ b/mcp/shared.ts @@ -1,7 +1,28 @@ +import { cached } from "@ark/util"; +import { Octokit } from "@octokit/rest"; import type { StandardSchemaV1 } from "@standard-schema/spec"; import type { FastMCP, Tool } from "fastmcp"; +import { parseRepoContext, type RepoContext } from "../utils/github.ts"; -export const tool = (tool: Tool<{}, StandardSchemaV1>) => tool; +export const getMcpContext = cached((): McpContext => { + const githubInstallationToken = process.env.GITHUB_INSTALLATION_TOKEN; + if (!githubInstallationToken) { + throw new Error("GITHUB_INSTALLATION_TOKEN environment variable is required"); + } + + return { + ...parseRepoContext(), + octokit: new Octokit({ + auth: githubInstallationToken, + }), + }; +}); + +export interface McpContext extends RepoContext { + octokit: Octokit; +} + +export const tool = (tool: Tool>) => tool; export const addTools = (server: FastMCP, tools: Tool[]) => { for (const tool of tools) { diff --git a/package.json b/package.json index 66bf0ae..9141146 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pullfrog/action", - "version": "0.0.50", + "version": "0.0.51", "type": "module", "files": [ "index.js", @@ -21,7 +21,8 @@ }, "dependencies": { "@actions/core": "^1.11.1", - "@ark/fs": "0.49.0", + "@ark/fs": "0.50.0", + "@ark/util": "0.50.0", "@octokit/rest": "^22.0.0", "@octokit/webhooks-types": "^7.6.1", "@standard-schema/spec": "1.0.0", diff --git a/utils/github.ts b/utils/github.ts index add25a6..f0091bf 100644 --- a/utils/github.ts +++ b/utils/github.ts @@ -1,6 +1,5 @@ import { createSign } from "node:crypto"; import * as core from "@actions/core"; -import { resolveRepoContext } from "./repo-context.ts"; export interface InstallationToken { token: string; @@ -55,7 +54,7 @@ function isGitHubActionsEnvironment(): boolean { async function acquireTokenViaOIDC(): Promise { core.info("Generating OIDC token..."); - + const oidcToken = await core.getIDToken("pullfrog-api"); core.info("OIDC token generated successfully"); @@ -208,7 +207,7 @@ const findInstallationId = async ( }; async function acquireTokenViaGitHubApp(): Promise { - const repoContext = resolveRepoContext(); + const repoContext = parseRepoContext(); const config: GitHubAppConfig = { appId: process.env.GITHUB_APP_ID!, @@ -244,9 +243,31 @@ export async function setupGitHubInstallationToken(): Promise { } const token = await acquireNewToken(); - + core.setSecret(token); process.env.GITHUB_INSTALLATION_TOKEN = token; - + return token; } + +export interface RepoContext { + owner: string; + name: string; +} + +/** + * Parse repository context from GITHUB_REPOSITORY environment variable. + */ +export function parseRepoContext(): RepoContext { + const githubRepo = process.env.GITHUB_REPOSITORY; + if (!githubRepo) { + throw new Error("GITHUB_REPOSITORY environment variable is required"); + } + + const [owner, name] = githubRepo.split("/"); + if (!owner || !name) { + throw new Error(`Invalid GITHUB_REPOSITORY format: ${githubRepo}. Expected 'owner/repo'`); + } + + return { owner, name }; +} diff --git a/utils/repo-context.ts b/utils/repo-context.ts deleted file mode 100644 index 684503c..0000000 --- a/utils/repo-context.ts +++ /dev/null @@ -1,22 +0,0 @@ -export interface RepoContext { - owner: string; - name: string; -} - -/** - * Resolve repository context from GITHUB_REPOSITORY environment variable. - * Throws if not available. - */ -export function resolveRepoContext(): RepoContext { - const githubRepo = process.env.GITHUB_REPOSITORY; - if (!githubRepo) { - throw new Error('GITHUB_REPOSITORY environment variable is required'); - } - - const [owner, name] = githubRepo.split('/'); - if (!owner || !name) { - throw new Error(`Invalid GITHUB_REPOSITORY format: ${githubRepo}. Expected 'owner/repo'`); - } - - return { owner, name }; -} \ No newline at end of file