refactor mcp and add instructions prefix

This commit is contained in:
David Blass
2025-10-17 22:26:24 -04:00
parent 7f1566d9c2
commit c668578c6f
12 changed files with 91 additions and 96 deletions
+20 -41
View File
@@ -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`],
+6
View File
@@ -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
`;
+2 -2
View File
@@ -29,6 +29,6 @@ export interface AgentResult {
* Configuration for agent creation
*/
export interface AgentConfig {
apiKey?: string;
[key: string]: any;
apiKey: string;
githubInstallationToken: string;
}
+1 -3
View File
@@ -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.
add a comment containing your best frog joke to GitHub issue https://github.com/pullfrogai/scratch/issues/2
+1
View File
@@ -27,6 +27,7 @@ export async function main(inputs: Inputs): Promise<MainResult> {
apiKey: inputs.anthropic_api_key!,
githubInstallationToken,
});
await agent.install();
const result = await agent.execute(inputs.prompt);
+6 -18
View File
@@ -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,
});
+3 -1
View File
@@ -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: {
+1 -1
View File
@@ -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",
});
+22 -1
View File
@@ -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 = <const params>(tool: Tool<{}, StandardSchemaV1<params>>) => 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 = <const params>(tool: Tool<any, StandardSchemaV1<params>>) => tool;
export const addTools = (server: FastMCP, tools: Tool<any, any>[]) => {
for (const tool of tools) {
+3 -2
View File
@@ -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",
+26 -5
View File
@@ -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<string> {
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<string> {
const repoContext = resolveRepoContext();
const repoContext = parseRepoContext();
const config: GitHubAppConfig = {
appId: process.env.GITHUB_APP_ID!,
@@ -244,9 +243,31 @@ export async function setupGitHubInstallationToken(): Promise<string> {
}
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 };
}
-22
View File
@@ -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 };
}