Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 706ce04895 | |||
| 09be8e3068 | |||
| c6c1210fa0 | |||
| 0368512b9e | |||
| 9fb6135fd2 | |||
| bb78e5f94b | |||
| c668578c6f |
+29
-42
@@ -3,6 +3,7 @@ import * as core from "@actions/core";
|
|||||||
import { createMcpConfig } from "../mcp/config.ts";
|
import { createMcpConfig } from "../mcp/config.ts";
|
||||||
import { spawn } from "../utils/subprocess.ts";
|
import { spawn } from "../utils/subprocess.ts";
|
||||||
import { boxString, tableString } from "../utils/table.ts";
|
import { boxString, tableString } from "../utils/table.ts";
|
||||||
|
import { instructions } from "./shared.ts";
|
||||||
import type { Agent, AgentConfig, AgentResult } from "./types.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 {
|
export class ClaudeAgent implements Agent {
|
||||||
private apiKey: string;
|
private apiKey: string;
|
||||||
private githubInstallationToken?: string;
|
private githubInstallationToken: string;
|
||||||
public runStats = {
|
public runStats = {
|
||||||
toolsUsed: 0,
|
toolsUsed: 0,
|
||||||
turns: 0,
|
turns: 0,
|
||||||
@@ -18,9 +19,6 @@ export class ClaudeAgent implements Agent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
constructor(config: AgentConfig) {
|
constructor(config: AgentConfig) {
|
||||||
if (!config.apiKey) {
|
|
||||||
throw new Error("Claude agent requires an API key");
|
|
||||||
}
|
|
||||||
this.apiKey = config.apiKey;
|
this.apiKey = config.apiKey;
|
||||||
this.githubInstallationToken = config.githubInstallationToken;
|
this.githubInstallationToken = config.githubInstallationToken;
|
||||||
}
|
}
|
||||||
@@ -51,10 +49,7 @@ export class ClaudeAgent implements Agent {
|
|||||||
try {
|
try {
|
||||||
const result = await spawn({
|
const result = await spawn({
|
||||||
cmd: "bash",
|
cmd: "bash",
|
||||||
args: [
|
args: ["-c", "curl -fsSL https://claude.ai/install.sh | bash -s 1.0.93"],
|
||||||
"-c",
|
|
||||||
"curl -fsSL https://claude.ai/install.sh | bash -s 1.0.93",
|
|
||||||
],
|
|
||||||
env: { ANTHROPIC_API_KEY: this.apiKey },
|
env: { ANTHROPIC_API_KEY: this.apiKey },
|
||||||
timeout: 120000, // 2 minute timeout
|
timeout: 120000, // 2 minute timeout
|
||||||
onStdout: () => {},
|
onStdout: () => {},
|
||||||
@@ -62,9 +57,7 @@ export class ClaudeAgent implements Agent {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (result.exitCode !== 0) {
|
if (result.exitCode !== 0) {
|
||||||
throw new Error(
|
throw new Error(`Installation failed with exit code ${result.exitCode}: ${result.stderr}`);
|
||||||
`Installation failed with exit code ${result.exitCode}: ${result.stderr}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
core.info("Claude Code installed successfully");
|
core.info("Claude Code installed successfully");
|
||||||
@@ -81,7 +74,17 @@ export class ClaudeAgent implements Agent {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const claudePath = `${process.env.HOME}/.local/bin/claude`;
|
const claudePath = `${process.env.HOME}/.local/bin/claude`;
|
||||||
|
|
||||||
|
const env = {
|
||||||
|
ANTHROPIC_API_KEY: this.apiKey,
|
||||||
|
GITHUB_TOKEN: this.githubInstallationToken,
|
||||||
|
};
|
||||||
|
|
||||||
console.log(boxString(prompt, { title: "Prompt" }));
|
console.log(boxString(prompt, { title: "Prompt" }));
|
||||||
|
|
||||||
|
const mcpConfig = createMcpConfig(this.githubInstallationToken);
|
||||||
|
console.log("📋 MCP Config:", mcpConfig);
|
||||||
|
|
||||||
const args = [
|
const args = [
|
||||||
"--print",
|
"--print",
|
||||||
"--output-format",
|
"--output-format",
|
||||||
@@ -90,22 +93,10 @@ export class ClaudeAgent implements Agent {
|
|||||||
"--debug",
|
"--debug",
|
||||||
"--permission-mode",
|
"--permission-mode",
|
||||||
"bypassPermissions",
|
"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");
|
core.startGroup("🔄 Run details");
|
||||||
|
|
||||||
this.runStats = {
|
this.runStats = {
|
||||||
@@ -121,7 +112,7 @@ export class ClaudeAgent implements Agent {
|
|||||||
cmd: claudePath,
|
cmd: claudePath,
|
||||||
args,
|
args,
|
||||||
env,
|
env,
|
||||||
input: prompt,
|
input: `${instructions} ${prompt}`,
|
||||||
timeout: 10 * 60 * 1000, // 10 minutes
|
timeout: 10 * 60 * 1000, // 10 minutes
|
||||||
onStdout: (_chunk) => {
|
onStdout: (_chunk) => {
|
||||||
processJSONChunk(_chunk, this);
|
processJSONChunk(_chunk, this);
|
||||||
@@ -161,8 +152,7 @@ export class ClaudeAgent implements Agent {
|
|||||||
try {
|
try {
|
||||||
core.endGroup();
|
core.endGroup();
|
||||||
} catch {}
|
} catch {}
|
||||||
const errorMessage =
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
||||||
error instanceof Error ? error.message : "Unknown error";
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: `Failed to execute Claude Code: ${errorMessage}`,
|
error: `Failed to execute Claude Code: ${errorMessage}`,
|
||||||
@@ -176,8 +166,15 @@ export class ClaudeAgent implements Agent {
|
|||||||
*/
|
*/
|
||||||
function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
||||||
try {
|
try {
|
||||||
|
// Skip debug lines that start with [DEBUG] or [debug]
|
||||||
|
const trimmedChunk = chunk.trim();
|
||||||
|
if (trimmedChunk.startsWith("[DEBUG]") || trimmedChunk.startsWith("[debug]")) {
|
||||||
|
console.log(chunk);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log(chunk);
|
console.log(chunk);
|
||||||
const parsedChunk = JSON.parse(chunk.trim());
|
const parsedChunk = JSON.parse(trimmedChunk);
|
||||||
|
|
||||||
switch (parsedChunk.type) {
|
switch (parsedChunk.type) {
|
||||||
case "system":
|
case "system":
|
||||||
@@ -188,12 +185,7 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
|||||||
["model", parsedChunk.model],
|
["model", parsedChunk.model],
|
||||||
["cwd", parsedChunk.cwd],
|
["cwd", parsedChunk.cwd],
|
||||||
["permission_mode", parsedChunk.permissionMode],
|
["permission_mode", parsedChunk.permissionMode],
|
||||||
[
|
["tools", parsedChunk.tools?.length ? `${parsedChunk.tools.length} tools` : "none"],
|
||||||
"tools",
|
|
||||||
parsedChunk.tools?.length
|
|
||||||
? `${parsedChunk.tools.length} tools`
|
|
||||||
: "none",
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
"mcp_servers",
|
"mcp_servers",
|
||||||
parsedChunk.mcp_servers?.length
|
parsedChunk.mcp_servers?.length
|
||||||
@@ -220,9 +212,7 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
|||||||
for (const content of parsedChunk.message.content) {
|
for (const content of parsedChunk.message.content) {
|
||||||
if (content.type === "text") {
|
if (content.type === "text") {
|
||||||
if (content.text.trim()) {
|
if (content.text.trim()) {
|
||||||
core.info(
|
core.info(boxString(content.text.trim(), { title: "Claude Code" }));
|
||||||
boxString(content.text.trim(), { title: "Claude Code" })
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else if (content.type === "tool_use") {
|
} else if (content.type === "tool_use") {
|
||||||
if (agent) {
|
if (agent) {
|
||||||
@@ -307,10 +297,7 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
|||||||
if (parsedChunk.subtype === "success") {
|
if (parsedChunk.subtype === "success") {
|
||||||
core.info(
|
core.info(
|
||||||
tableString([
|
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],
|
["Input Tokens", parsedChunk.usage?.input_tokens || 0],
|
||||||
["Output Tokens", parsedChunk.usage?.output_tokens || 0],
|
["Output Tokens", parsedChunk.usage?.output_tokens || 0],
|
||||||
["Duration", `${parsedChunk.duration_ms}ms`],
|
["Duration", `${parsedChunk.duration_ms}ms`],
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
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
|
||||||
|
- if prompted by a comment to respond to create a new issue, pr or anything else, after succeeding,
|
||||||
|
also respond to the original comment with a very brief message containing a link to it
|
||||||
|
`;
|
||||||
+2
-2
@@ -29,6 +29,6 @@ export interface AgentResult {
|
|||||||
* Configuration for agent creation
|
* Configuration for agent creation
|
||||||
*/
|
*/
|
||||||
export interface AgentConfig {
|
export interface AgentConfig {
|
||||||
apiKey?: string;
|
apiKey: string;
|
||||||
[key: string]: any;
|
githubInstallationToken: string;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-3
@@ -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.
|
create a PR implementing bogosort to https://github.com/pullfrogai/scratch/
|
||||||
|
|
||||||
Do not use the gh cli. If the mcp tool does not work, bail.
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export async function main(inputs: Inputs): Promise<MainResult> {
|
|||||||
apiKey: inputs.anthropic_api_key!,
|
apiKey: inputs.anthropic_api_key!,
|
||||||
githubInstallationToken,
|
githubInstallationToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
await agent.install();
|
await agent.install();
|
||||||
|
|
||||||
const result = await agent.execute(inputs.prompt);
|
const result = await agent.execute(inputs.prompt);
|
||||||
|
|||||||
+15
-53
@@ -1,7 +1,5 @@
|
|||||||
import { Octokit } from "@octokit/rest";
|
|
||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import { resolveRepoContext } from "../utils/repo-context.ts";
|
import { contextualize, tool } from "./shared.ts";
|
||||||
import { tool } from "./shared.ts";
|
|
||||||
|
|
||||||
export const Comment = type({
|
export const Comment = type({
|
||||||
issueNumber: type.number.describe("the issue number to comment on"),
|
issueNumber: type.number.describe("the issue number to comment on"),
|
||||||
@@ -12,55 +10,19 @@ export const CommentTool = tool({
|
|||||||
name: "create_issue_comment",
|
name: "create_issue_comment",
|
||||||
description: "Create a comment on a GitHub issue",
|
description: "Create a comment on a GitHub issue",
|
||||||
parameters: Comment,
|
parameters: Comment,
|
||||||
execute: async ({ issueNumber, body }: { issueNumber: number; body: string }) => {
|
execute: contextualize(async ({ issueNumber, body }, ctx) => {
|
||||||
try {
|
const result = await ctx.octokit.rest.issues.createComment({
|
||||||
const githubInstallationToken = process.env.GITHUB_INSTALLATION_TOKEN;
|
owner: ctx.owner,
|
||||||
if (!githubInstallationToken) {
|
repo: ctx.name,
|
||||||
throw new Error("GITHUB_INSTALLATION_TOKEN environment variable is required");
|
issue_number: issueNumber,
|
||||||
}
|
body: body,
|
||||||
|
});
|
||||||
|
|
||||||
const repoContext = resolveRepoContext();
|
return {
|
||||||
|
success: true,
|
||||||
const octokit = new Octokit({
|
commentId: result.data.id,
|
||||||
auth: githubInstallationToken,
|
url: result.data.html_url,
|
||||||
});
|
body: result.data.body,
|
||||||
|
};
|
||||||
const result = await octokit.rest.issues.createComment({
|
}),
|
||||||
owner: repoContext.owner,
|
|
||||||
repo: repoContext.name,
|
|
||||||
issue_number: issueNumber,
|
|
||||||
body: body,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: JSON.stringify(
|
|
||||||
{
|
|
||||||
success: true,
|
|
||||||
commentId: result.data.id,
|
|
||||||
url: result.data.html_url,
|
|
||||||
body: result.data.body,
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: `Error creating comment: ${errorMessage}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
error: errorMessage,
|
|
||||||
isError: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
+3
-1
@@ -5,6 +5,8 @@ import { fromHere } from "@ark/fs";
|
|||||||
|
|
||||||
const actionPath = fromHere("..");
|
const actionPath = fromHere("..");
|
||||||
|
|
||||||
|
export const mcpServerName = "gh-pullfrog";
|
||||||
|
|
||||||
export function createMcpConfig(githubInstallationToken: string) {
|
export function createMcpConfig(githubInstallationToken: string) {
|
||||||
const githubRepository = process.env.GITHUB_REPOSITORY;
|
const githubRepository = process.env.GITHUB_REPOSITORY;
|
||||||
if (!githubRepository) {
|
if (!githubRepository) {
|
||||||
@@ -16,7 +18,7 @@ export function createMcpConfig(githubInstallationToken: string) {
|
|||||||
return JSON.stringify(
|
return JSON.stringify(
|
||||||
{
|
{
|
||||||
mcpServers: {
|
mcpServers: {
|
||||||
minimal_github_comment: {
|
[mcpServerName]: {
|
||||||
command: "node",
|
command: "node",
|
||||||
args: [`${actionPath}/mcp/server.ts`],
|
args: [`${actionPath}/mcp/server.ts`],
|
||||||
env: {
|
env: {
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { type } from "arktype";
|
||||||
|
import { contextualize, tool } from "./shared.ts";
|
||||||
|
|
||||||
|
export const Issue = type({
|
||||||
|
title: type.string.describe("the title of the issue"),
|
||||||
|
body: type.string.describe("the body content of the issue"),
|
||||||
|
labels: type.string
|
||||||
|
.array()
|
||||||
|
.describe("optional array of label names to apply to the issue")
|
||||||
|
.optional(),
|
||||||
|
assignees: type.string
|
||||||
|
.array()
|
||||||
|
.describe("optional array of usernames to assign to the issue")
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const IssueTool = tool({
|
||||||
|
name: "create_issue",
|
||||||
|
description: "Create a new GitHub issue",
|
||||||
|
parameters: Issue,
|
||||||
|
execute: contextualize(async ({ title, body, labels, assignees }, ctx) => {
|
||||||
|
const result = await ctx.octokit.rest.issues.create({
|
||||||
|
owner: ctx.owner,
|
||||||
|
repo: ctx.name,
|
||||||
|
title: title,
|
||||||
|
body: body,
|
||||||
|
labels: labels ?? [],
|
||||||
|
assignees: assignees ?? [],
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
issueId: result.data.id,
|
||||||
|
number: result.data.number,
|
||||||
|
url: result.data.html_url,
|
||||||
|
title: result.data.title,
|
||||||
|
state: result.data.state,
|
||||||
|
labels: result.data.labels?.map((label) => (typeof label === "string" ? label : label.name)),
|
||||||
|
assignees: result.data.assignees?.map((assignee) => assignee.login),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { execSync } from "node:child_process";
|
||||||
|
import { type } from "arktype";
|
||||||
|
import { contextualize, tool } from "./shared.ts";
|
||||||
|
|
||||||
|
export const PullRequest = type({
|
||||||
|
title: type.string.describe("the title of the pull request"),
|
||||||
|
body: type.string.describe("the body content of the pull request"),
|
||||||
|
base: type.string.describe("the base branch to merge into (e.g., 'main')"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const PullRequestTool = tool({
|
||||||
|
name: "create_pull_request",
|
||||||
|
description: "Create a pull request from the current branch",
|
||||||
|
parameters: PullRequest,
|
||||||
|
execute: contextualize(async ({ title, body, base }, ctx) => {
|
||||||
|
// Get the current branch name
|
||||||
|
const currentBranch = execSync("git rev-parse --abbrev-ref HEAD", {
|
||||||
|
encoding: "utf8",
|
||||||
|
}).trim();
|
||||||
|
|
||||||
|
console.log(`Current branch: ${currentBranch}`);
|
||||||
|
|
||||||
|
const result = await ctx.octokit.rest.pulls.create({
|
||||||
|
owner: ctx.owner,
|
||||||
|
repo: ctx.name,
|
||||||
|
title: title,
|
||||||
|
body: body,
|
||||||
|
head: currentBranch,
|
||||||
|
base: base,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
pullRequestId: result.data.id,
|
||||||
|
number: result.data.number,
|
||||||
|
url: result.data.html_url,
|
||||||
|
title: result.data.title,
|
||||||
|
head: result.data.head.ref,
|
||||||
|
base: result.data.base.ref,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
+4
-2
@@ -2,13 +2,15 @@
|
|||||||
// Minimal GitHub Issue Comment MCP Server
|
// Minimal GitHub Issue Comment MCP Server
|
||||||
import { FastMCP } from "fastmcp";
|
import { FastMCP } from "fastmcp";
|
||||||
import { CommentTool } from "./comment.ts";
|
import { CommentTool } from "./comment.ts";
|
||||||
|
import { IssueTool } from "./issue.ts";
|
||||||
|
import { PullRequestTool } from "./pr.ts";
|
||||||
import { addTools } from "./shared.ts";
|
import { addTools } from "./shared.ts";
|
||||||
|
|
||||||
const server = new FastMCP({
|
const server = new FastMCP({
|
||||||
name: "Minimal GitHub Issue Comment Server",
|
name: "gh-pullfrog",
|
||||||
version: "0.0.1",
|
version: "0.0.1",
|
||||||
});
|
});
|
||||||
|
|
||||||
addTools(server, [CommentTool]);
|
addTools(server, [CommentTool, IssueTool, PullRequestTool]);
|
||||||
|
|
||||||
server.start();
|
server.start();
|
||||||
|
|||||||
+68
-1
@@ -1,7 +1,37 @@
|
|||||||
|
import { cached } from "@ark/util";
|
||||||
|
import { Octokit } from "@octokit/rest";
|
||||||
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
||||||
import type { FastMCP, Tool } from "fastmcp";
|
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 interface ToolResult {
|
||||||
|
content: {
|
||||||
|
type: "text";
|
||||||
|
text: string;
|
||||||
|
}[];
|
||||||
|
error?: string;
|
||||||
|
isError?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
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>[]) => {
|
export const addTools = (server: FastMCP, tools: Tool<any, any>[]) => {
|
||||||
for (const tool of tools) {
|
for (const tool of tools) {
|
||||||
@@ -9,3 +39,40 @@ export const addTools = (server: FastMCP, tools: Tool<any, any>[]) => {
|
|||||||
}
|
}
|
||||||
return server;
|
return server;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const contextualize =
|
||||||
|
<T>(executor: (params: T, ctx: McpContext) => Promise<Record<string, any>>) =>
|
||||||
|
async (params: T): Promise<ToolResult> => {
|
||||||
|
try {
|
||||||
|
const ctx = getMcpContext();
|
||||||
|
const result = await executor(params, ctx);
|
||||||
|
return handleToolSuccess(result);
|
||||||
|
} catch (error) {
|
||||||
|
return handleToolError(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToolSuccess = (data: Record<string, any>): ToolResult => {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(data, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToolError = (error: unknown): ToolResult => {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Error: ${errorMessage}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
error: errorMessage,
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
+3
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@pullfrog/action",
|
"name": "@pullfrog/action",
|
||||||
"version": "0.0.50",
|
"version": "0.0.55",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"index.js",
|
"index.js",
|
||||||
@@ -21,7 +21,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.11.1",
|
"@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/rest": "^22.0.0",
|
||||||
"@octokit/webhooks-types": "^7.6.1",
|
"@octokit/webhooks-types": "^7.6.1",
|
||||||
"@standard-schema/spec": "1.0.0",
|
"@standard-schema/spec": "1.0.0",
|
||||||
|
|||||||
Generated
+8
-5
@@ -12,8 +12,11 @@ importers:
|
|||||||
specifier: ^1.11.1
|
specifier: ^1.11.1
|
||||||
version: 1.11.1
|
version: 1.11.1
|
||||||
'@ark/fs':
|
'@ark/fs':
|
||||||
specifier: 0.49.0
|
specifier: 0.50.0
|
||||||
version: 0.49.0
|
version: 0.50.0
|
||||||
|
'@ark/util':
|
||||||
|
specifier: 0.50.0
|
||||||
|
version: 0.50.0
|
||||||
'@octokit/rest':
|
'@octokit/rest':
|
||||||
specifier: ^22.0.0
|
specifier: ^22.0.0
|
||||||
version: 22.0.0
|
version: 22.0.0
|
||||||
@@ -63,8 +66,8 @@ packages:
|
|||||||
'@actions/io@1.1.3':
|
'@actions/io@1.1.3':
|
||||||
resolution: {integrity: sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==}
|
resolution: {integrity: sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==}
|
||||||
|
|
||||||
'@ark/fs@0.49.0':
|
'@ark/fs@0.50.0':
|
||||||
resolution: {integrity: sha512-AEjAQS/bu1CGIRiKK/XLaQ73cSJHixfexq28wNt+kBpQ0h1RwVIVzaGsn/+5IWw6DEbR7LB+3hil5gzrzEeyZQ==}
|
resolution: {integrity: sha512-6OrxNt2T+/pL4RUMZK/aiVRLIS3acNs5uSpHDyZAP6+OXwXchFSQ1lJTH+uuGBlDWeQxPD7VmymYXLF5s1Eyhw==}
|
||||||
|
|
||||||
'@ark/regex@0.0.0':
|
'@ark/regex@0.0.0':
|
||||||
resolution: {integrity: sha512-p4vsWnd/LRGOdGQglbwOguIVhPmCAf5UzquvnDoxqhhPWTP84wWgi1INea8MgJ4SnI2gp37f13oA4Waz9vwNYg==}
|
resolution: {integrity: sha512-p4vsWnd/LRGOdGQglbwOguIVhPmCAf5UzquvnDoxqhhPWTP84wWgi1INea8MgJ4SnI2gp37f13oA4Waz9vwNYg==}
|
||||||
@@ -792,7 +795,7 @@ snapshots:
|
|||||||
|
|
||||||
'@actions/io@1.1.3': {}
|
'@actions/io@1.1.3': {}
|
||||||
|
|
||||||
'@ark/fs@0.49.0': {}
|
'@ark/fs@0.50.0': {}
|
||||||
|
|
||||||
'@ark/regex@0.0.0':
|
'@ark/regex@0.0.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
+23
-2
@@ -1,6 +1,5 @@
|
|||||||
import { createSign } from "node:crypto";
|
import { createSign } from "node:crypto";
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import { resolveRepoContext } from "./repo-context.ts";
|
|
||||||
|
|
||||||
export interface InstallationToken {
|
export interface InstallationToken {
|
||||||
token: string;
|
token: string;
|
||||||
@@ -208,7 +207,7 @@ const findInstallationId = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function acquireTokenViaGitHubApp(): Promise<string> {
|
async function acquireTokenViaGitHubApp(): Promise<string> {
|
||||||
const repoContext = resolveRepoContext();
|
const repoContext = parseRepoContext();
|
||||||
|
|
||||||
const config: GitHubAppConfig = {
|
const config: GitHubAppConfig = {
|
||||||
appId: process.env.GITHUB_APP_ID!,
|
appId: process.env.GITHUB_APP_ID!,
|
||||||
@@ -250,3 +249,25 @@ export async function setupGitHubInstallationToken(): Promise<string> {
|
|||||||
|
|
||||||
return 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 };
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user