Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d88bfce42 | |||
| c8cbda6972 | |||
| 4ff547f673 | |||
| 106de07802 | |||
| aba21e7583 | |||
| ff375b97e4 |
+1
-1
@@ -8,5 +8,5 @@ if git diff --cached --name-only | grep -q "^package.json$"; then
|
||||
pnpm build
|
||||
|
||||
# Add the built files and lockfile to the commit
|
||||
git add entry mcp-server pnpm-lock.yaml
|
||||
git add entry pnpm-lock.yaml
|
||||
fi
|
||||
|
||||
+19
-23
@@ -128,7 +128,7 @@ const messageHandlers: {
|
||||
toolName: item.tool,
|
||||
input: {
|
||||
server: item.server,
|
||||
...((item as any).args || {}),
|
||||
...((item as any).arguments || {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -167,7 +167,7 @@ const messageHandlers: {
|
||||
const reasoningText = item.text.trim();
|
||||
// Remove markdown bold markers if present for cleaner output
|
||||
const cleanText = reasoningText.replace(/\*\*/g, "");
|
||||
log.info(cleanText);
|
||||
log.box(cleanText, { title: "Codex" });
|
||||
}
|
||||
},
|
||||
error: (event) => {
|
||||
@@ -177,34 +177,30 @@ const messageHandlers: {
|
||||
|
||||
/**
|
||||
* Configure MCP servers for Codex using the CLI.
|
||||
* Codex CLI syntax: codex mcp add <name> --env KEY=value -- <command> [args...]
|
||||
* For HTTP-based servers, use: codex mcp add <name> --url <url>
|
||||
*/
|
||||
function configureCodexMcpServers({ mcpServers, cliPath }: ConfigureMcpServersParams): void {
|
||||
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
||||
const command = serverConfig.command;
|
||||
const args = serverConfig.args || [];
|
||||
const envVars = serverConfig.env || {};
|
||||
if (serverConfig.type === "http") {
|
||||
// HTTP-based MCP server - use --url flag
|
||||
const addArgs = ["mcp", "add", serverName, "--url", serverConfig.url];
|
||||
|
||||
const addArgs = ["mcp", "add", serverName];
|
||||
log.info(`Adding MCP server '${serverName}' at ${serverConfig.url}...`);
|
||||
const addResult = spawnSync("node", [cliPath, ...addArgs], {
|
||||
stdio: "pipe",
|
||||
encoding: "utf-8",
|
||||
});
|
||||
|
||||
// Add environment variables as --env flags first
|
||||
for (const [key, value] of Object.entries(envVars)) {
|
||||
addArgs.push("--env", `${key}=${value}`);
|
||||
}
|
||||
|
||||
addArgs.push("--", command, ...args);
|
||||
|
||||
log.info(`Adding MCP server '${serverName}'...`);
|
||||
const addResult = spawnSync("node", [cliPath, ...addArgs], {
|
||||
stdio: "pipe",
|
||||
encoding: "utf-8",
|
||||
});
|
||||
|
||||
if (addResult.status !== 0) {
|
||||
if (addResult.status !== 0) {
|
||||
throw new Error(
|
||||
`codex mcp add failed: ${addResult.stderr || addResult.stdout || "Unknown error"}`
|
||||
);
|
||||
}
|
||||
log.info(`✓ MCP server '${serverName}' configured`);
|
||||
} else {
|
||||
throw new Error(
|
||||
`codex mcp add failed: ${addResult.stderr || addResult.stdout || "Unknown error"}`
|
||||
`Unsupported MCP server type for Codex: ${(serverConfig as any).type || "unknown"}`
|
||||
);
|
||||
}
|
||||
log.info(`✓ MCP server '${serverName}' configured`);
|
||||
}
|
||||
}
|
||||
|
||||
+17
-1
@@ -269,6 +269,22 @@ function configureCursorMcpServers({ mcpServers }: ConfigureMcpServersParams) {
|
||||
const cursorConfigDir = join(realHome, ".cursor");
|
||||
const mcpConfigPath = join(cursorConfigDir, "mcp.json");
|
||||
mkdirSync(cursorConfigDir, { recursive: true });
|
||||
writeFileSync(mcpConfigPath, JSON.stringify({ mcpServers }, null, 2), "utf-8");
|
||||
|
||||
// Convert to Cursor's expected format (HTTP config)
|
||||
const cursorMcpServers: Record<string, { type: string; url: string }> = {};
|
||||
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
||||
if (serverConfig.type !== "http") {
|
||||
throw new Error(
|
||||
`Unsupported MCP server type for Cursor: ${(serverConfig as any).type || "unknown"}`
|
||||
);
|
||||
}
|
||||
|
||||
cursorMcpServers[serverName] = {
|
||||
type: "http",
|
||||
url: serverConfig.url,
|
||||
};
|
||||
}
|
||||
|
||||
writeFileSync(mcpConfigPath, JSON.stringify({ mcpServers: cursorMcpServers }, null, 2), "utf-8");
|
||||
log.info(`MCP config written to ${mcpConfigPath}`);
|
||||
}
|
||||
|
||||
+18
-18
@@ -249,31 +249,31 @@ export const gemini = agent({
|
||||
|
||||
/**
|
||||
* Configure MCP servers for Gemini using the CLI.
|
||||
* Gemini CLI syntax: gemini mcp add <name> <commandOrUrl> [args...] --env KEY=value
|
||||
* Gemini CLI syntax: gemini mcp add <name> <commandOrUrl> [args...] --transport <type>
|
||||
* For HTTP-based servers, use: gemini mcp add <name> <url> --transport http
|
||||
*/
|
||||
function configureGeminiMcpServers({ mcpServers, cliPath }: ConfigureMcpServersParams): void {
|
||||
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
||||
const command = serverConfig.command;
|
||||
const args = serverConfig.args || [];
|
||||
const envVars = serverConfig.env || {};
|
||||
if (serverConfig.type === "http") {
|
||||
// HTTP-based MCP server - use URL with --transport http flag
|
||||
const addArgs = ["mcp", "add", serverName, serverConfig.url, "--transport", "http"];
|
||||
|
||||
const addArgs = ["mcp", "add", serverName, command, ...args];
|
||||
log.info(`Adding MCP server '${serverName}' at ${serverConfig.url}...`);
|
||||
const addResult = spawnSync("node", [cliPath, ...addArgs], {
|
||||
stdio: "pipe",
|
||||
encoding: "utf-8",
|
||||
});
|
||||
|
||||
for (const [key, value] of Object.entries(envVars)) {
|
||||
addArgs.push("--env", `${key}=${value}`);
|
||||
}
|
||||
|
||||
log.info(`Adding MCP server '${serverName}'...`);
|
||||
const addResult = spawnSync("node", [cliPath, ...addArgs], {
|
||||
stdio: "pipe",
|
||||
encoding: "utf-8",
|
||||
});
|
||||
|
||||
if (addResult.status !== 0) {
|
||||
if (addResult.status !== 0) {
|
||||
throw new Error(
|
||||
`gemini mcp add failed: ${addResult.stderr || addResult.stdout || "Unknown error"}`
|
||||
);
|
||||
}
|
||||
log.info(`✓ MCP server '${serverName}' configured`);
|
||||
} else {
|
||||
throw new Error(
|
||||
`gemini mcp add failed: ${addResult.stderr || addResult.stdout || "Unknown error"}`
|
||||
`Unsupported MCP server type for Gemini: ${(serverConfig as any).type || "unknown"}`
|
||||
);
|
||||
}
|
||||
log.info(`✓ MCP server '${serverName}' configured`);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -4,7 +4,7 @@ import { mkdtemp } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { pipeline } from "node:stream/promises";
|
||||
import type { McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { McpHttpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { show } from "@ark/util";
|
||||
import { type AgentManifest, type AgentName, agentsManifest, type Payload } from "../external.ts";
|
||||
import { log } from "../utils/cli.ts";
|
||||
@@ -26,7 +26,7 @@ export interface AgentConfig {
|
||||
apiKey: string;
|
||||
githubInstallationToken: string;
|
||||
payload: Payload;
|
||||
mcpServers: Record<string, McpStdioServerConfig>;
|
||||
mcpServers: Record<string, McpHttpServerConfig>;
|
||||
cliPath: string;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export interface AgentConfig {
|
||||
* Parameters for configuring MCP servers
|
||||
*/
|
||||
export interface ConfigureMcpServersParams {
|
||||
mcpServers: Record<string, McpStdioServerConfig>;
|
||||
mcpServers: Record<string, McpHttpServerConfig>;
|
||||
cliPath: string;
|
||||
}
|
||||
|
||||
|
||||
+1
-9
@@ -59,7 +59,7 @@ const sharedConfig = {
|
||||
drop: [],
|
||||
};
|
||||
|
||||
// Build the main entry bundle (without MCP)
|
||||
// Build the main entry bundle
|
||||
await build({
|
||||
...sharedConfig,
|
||||
entryPoints: ["./entry.ts"],
|
||||
@@ -67,12 +67,4 @@ await build({
|
||||
plugins: [stripShebangPlugin],
|
||||
});
|
||||
|
||||
// Build the MCP server bundle
|
||||
await build({
|
||||
...sharedConfig,
|
||||
entryPoints: ["./mcp/server.ts"],
|
||||
outfile: "./mcp-server",
|
||||
plugins: [stripShebangPlugin],
|
||||
});
|
||||
|
||||
console.log("✅ Build completed successfully!");
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
write a comment to https://github.com/pullfrogai/scratch/pull/29 that tells a joke
|
||||
use debug_shell_command tool to run 'git status' and explain the result
|
||||
@@ -10,6 +10,7 @@ import type { AgentResult } from "./agents/shared.ts";
|
||||
import type { AgentName, AgentName as AgentNameType, Payload } from "./external.ts";
|
||||
import { agentsManifest } from "./external.ts";
|
||||
import { createMcpConfigs } from "./mcp/config.ts";
|
||||
import { startMcpHttpServer } from "./mcp/server.ts";
|
||||
import { modes } from "./modes.ts";
|
||||
import packageJson from "./package.json" with { type: "json" };
|
||||
import { fetchRepoSettings } from "./utils/api.ts";
|
||||
@@ -48,39 +49,29 @@ export interface MainResult {
|
||||
}
|
||||
|
||||
export async function main(inputs: Inputs): Promise<MainResult> {
|
||||
let githubInstallationToken: string | undefined;
|
||||
let pollInterval: NodeJS.Timeout | null = null;
|
||||
let mcpServerClose: (() => Promise<void>) | undefined;
|
||||
let githubInstallationToken: string | undefined;
|
||||
|
||||
try {
|
||||
// parse payload early to extract agent
|
||||
const payload = parsePayload(inputs);
|
||||
|
||||
// resolve agent before initializing context
|
||||
githubInstallationToken = await setupGitHubInstallationToken();
|
||||
const repoContext = parseRepoContext();
|
||||
const { agentName, agent } = await resolveAgent(
|
||||
inputs,
|
||||
payload,
|
||||
githubInstallationToken,
|
||||
repoContext
|
||||
);
|
||||
|
||||
const partialCtx = await initializeContext(
|
||||
inputs,
|
||||
agentName,
|
||||
agent,
|
||||
githubInstallationToken,
|
||||
repoContext
|
||||
);
|
||||
const partialCtx = await initializeContext(inputs, payload);
|
||||
const ctx = partialCtx as MainContext;
|
||||
ctx.payload = payload;
|
||||
githubInstallationToken = ctx.githubInstallationToken;
|
||||
|
||||
setupGitAuth(ctx.githubInstallationToken, ctx.repoContext);
|
||||
setupGitAuth({
|
||||
githubInstallationToken: ctx.githubInstallationToken,
|
||||
repoContext: ctx.repoContext,
|
||||
});
|
||||
await setupTempDirectory(ctx);
|
||||
setupMcpLogPolling(ctx);
|
||||
pollInterval = ctx.pollInterval;
|
||||
|
||||
setupGitBranch(ctx.payload);
|
||||
await startMcpServer(ctx);
|
||||
mcpServerClose = ctx.mcpServerClose;
|
||||
setupMcpServers(ctx);
|
||||
await installAgentCli(ctx);
|
||||
validateApiKey(ctx);
|
||||
@@ -99,6 +90,9 @@ export async function main(inputs: Inputs): Promise<MainResult> {
|
||||
if (pollInterval) {
|
||||
clearInterval(pollInterval);
|
||||
}
|
||||
if (mcpServerClose) {
|
||||
await mcpServerClose();
|
||||
}
|
||||
if (githubInstallationToken) {
|
||||
await revokeInstallationToken(githubInstallationToken);
|
||||
}
|
||||
@@ -179,6 +173,8 @@ interface MainContext {
|
||||
mcpLogPath: string;
|
||||
pollInterval: NodeJS.Timeout | null;
|
||||
payload: Payload;
|
||||
mcpServerUrl: string;
|
||||
mcpServerClose: () => Promise<void>;
|
||||
mcpServers: ReturnType<typeof createMcpConfigs>;
|
||||
cliPath: string;
|
||||
apiKey: string;
|
||||
@@ -186,21 +182,33 @@ interface MainContext {
|
||||
|
||||
async function initializeContext(
|
||||
inputs: Inputs,
|
||||
agentName: AgentNameType,
|
||||
agent: (typeof agents)[AgentNameType],
|
||||
githubInstallationToken: string,
|
||||
repoContext: RepoContext
|
||||
): Promise<Omit<MainContext, "payload" | "mcpServers" | "cliPath" | "apiKey">> {
|
||||
payload: Payload
|
||||
): Promise<
|
||||
Omit<MainContext, "mcpServerUrl" | "mcpServerClose" | "mcpServers" | "cliPath" | "apiKey">
|
||||
> {
|
||||
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
|
||||
Inputs.assert(inputs);
|
||||
setupGitConfig();
|
||||
|
||||
const githubInstallationToken = await setupGitHubInstallationToken();
|
||||
const repoContext = parseRepoContext();
|
||||
|
||||
// resolve agent and update payload with resolved agent name
|
||||
const { agentName, agent } = await resolveAgent(
|
||||
inputs,
|
||||
payload,
|
||||
githubInstallationToken,
|
||||
repoContext
|
||||
);
|
||||
const resolvedPayload = { ...payload, agent: agentName };
|
||||
|
||||
return {
|
||||
inputs,
|
||||
githubInstallationToken,
|
||||
repoContext,
|
||||
agentName,
|
||||
agent,
|
||||
payload: resolvedPayload,
|
||||
sharedTempDir: "",
|
||||
mcpLogPath: "",
|
||||
pollInterval: null,
|
||||
@@ -293,9 +301,27 @@ function parsePayload(inputs: Inputs): Payload {
|
||||
}
|
||||
}
|
||||
|
||||
function setupMcpServers(ctx: MainContext): void {
|
||||
async function startMcpServer(ctx: MainContext): Promise<void> {
|
||||
// Set environment variables for MCP server tools
|
||||
const repoContext = parseRepoContext();
|
||||
const githubRepository = `${repoContext.owner}/${repoContext.name}`;
|
||||
const allModes = [...modes, ...(ctx.payload.modes || [])];
|
||||
ctx.mcpServers = createMcpConfigs(ctx.githubInstallationToken, allModes, ctx.payload);
|
||||
|
||||
process.env.GITHUB_INSTALLATION_TOKEN = ctx.githubInstallationToken;
|
||||
process.env.GITHUB_REPOSITORY = githubRepository;
|
||||
process.env.PULLFROG_MODES = JSON.stringify(allModes);
|
||||
process.env.PULLFROG_PAYLOAD = JSON.stringify(ctx.payload);
|
||||
|
||||
// GITHUB_RUN_ID is already set in GitHub Actions, no need to set it here
|
||||
|
||||
const { url, close } = await startMcpHttpServer();
|
||||
ctx.mcpServerUrl = url;
|
||||
ctx.mcpServerClose = close;
|
||||
log.info(`🚀 MCP server started at ${url}`);
|
||||
}
|
||||
|
||||
function setupMcpServers(ctx: MainContext): void {
|
||||
ctx.mcpServers = createMcpConfigs(ctx.mcpServerUrl);
|
||||
log.debug(`📋 MCP Config: ${JSON.stringify(ctx.mcpServers, null, 2)}`);
|
||||
}
|
||||
|
||||
|
||||
-107871
File diff suppressed because one or more lines are too long
+1
-1
@@ -15,7 +15,7 @@ function buildCommentFooter(payload: Payload): string {
|
||||
const agentDisplayName = agentInfo?.displayName || "Unknown Agent";
|
||||
const agentUrl = agentInfo?.url || "https://pullfrog.ai";
|
||||
|
||||
// build workflow run URL
|
||||
// build workflow run URL: https://github.com/{owner}/{repo}/actions/runs/{runId}
|
||||
const workflowRunUrl = runId
|
||||
? `https://github.com/${repoContext.owner}/${repoContext.name}/actions/runs/${runId}`
|
||||
: `https://github.com/${repoContext.owner}/${repoContext.name}`;
|
||||
|
||||
+5
-34
@@ -2,47 +2,18 @@
|
||||
* Simple MCP configuration helper for adding our minimal GitHub comment server
|
||||
*/
|
||||
|
||||
import type { McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
||||
import { fromHere } from "@ark/fs";
|
||||
import type { Payload } from "../external.ts";
|
||||
import type { McpHttpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
||||
import { ghPullfrogMcpName } from "../external.ts";
|
||||
import type { Mode } from "../modes.ts";
|
||||
import { parseRepoContext } from "../utils/github.ts";
|
||||
|
||||
export type McpName = typeof ghPullfrogMcpName;
|
||||
|
||||
export type McpConfigs = Record<McpName, McpStdioServerConfig>;
|
||||
|
||||
export function createMcpConfigs(
|
||||
githubInstallationToken: string,
|
||||
modes: Mode[],
|
||||
payload: Payload
|
||||
): McpConfigs {
|
||||
const repoContext = parseRepoContext();
|
||||
const githubRepository = `${repoContext.owner}/${repoContext.name}`;
|
||||
|
||||
// In production (GitHub Actions), mcp-server is in same directory as entry.js (where this is bundled)
|
||||
// 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<string, string> = {
|
||||
GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
|
||||
GITHUB_REPOSITORY: githubRepository,
|
||||
PULLFROG_MODES: JSON.stringify(modes),
|
||||
PULLFROG_PAYLOAD: JSON.stringify(payload),
|
||||
PULLFROG_TEMP_DIR: process.env.PULLFROG_TEMP_DIR!,
|
||||
};
|
||||
|
||||
// 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;
|
||||
}
|
||||
export type McpConfigs = Record<McpName, McpHttpServerConfig>;
|
||||
|
||||
export function createMcpConfigs(mcpServerUrl: string): McpConfigs {
|
||||
return {
|
||||
[ghPullfrogMcpName]: {
|
||||
command: "node",
|
||||
args: [serverPath],
|
||||
env,
|
||||
type: "http",
|
||||
url: mcpServerUrl,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { type } from "arktype";
|
||||
import { $ } from "../utils/shell.ts";
|
||||
import type { ToolResult } from "./shared.ts";
|
||||
import { tool } from "./shared.ts";
|
||||
|
||||
export const DebugShellCommand = type({});
|
||||
|
||||
export const DebugShellCommandTool = tool({
|
||||
name: "debug_shell_command",
|
||||
description:
|
||||
"debug tool: runs 'git status' and returns the output. use this to test shell command execution in the MCP server.",
|
||||
parameters: DebugShellCommand,
|
||||
execute: async (): Promise<ToolResult> => {
|
||||
try {
|
||||
const result = $("git", ["status"]);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(
|
||||
{
|
||||
success: true,
|
||||
command: "git status",
|
||||
output: result.trim(),
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
+76
-19
@@ -1,5 +1,6 @@
|
||||
import "./arkConfig.ts";
|
||||
// this must be imported first
|
||||
import { createServer } from "node:net";
|
||||
import { FastMCP } from "fastmcp";
|
||||
import { ghPullfrogMcpName } from "../external.ts";
|
||||
import { GetCheckSuiteLogsTool } from "./checkSuite.ts";
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
EditCommentTool,
|
||||
UpdateWorkingCommentTool,
|
||||
} from "./comment.ts";
|
||||
import { DebugShellCommandTool } from "./debug.ts";
|
||||
import { IssueTool } from "./issue.ts";
|
||||
import { PullRequestTool } from "./pr.ts";
|
||||
import { PullRequestInfoTool } from "./prInfo.ts";
|
||||
@@ -17,24 +19,79 @@ import { GetReviewCommentsTool, ListPullRequestReviewsTool } from "./reviewComme
|
||||
import { SelectModeTool } from "./selectMode.ts";
|
||||
import { addTools } from "./shared.ts";
|
||||
|
||||
const server = new FastMCP({
|
||||
name: ghPullfrogMcpName,
|
||||
version: "0.0.1",
|
||||
});
|
||||
/**
|
||||
* Find an available port starting from the given port
|
||||
*/
|
||||
async function findAvailablePort(startPort: number): Promise<number> {
|
||||
const checkPort = (port: number): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
const server = createServer();
|
||||
server.once("error", () => {
|
||||
server.close();
|
||||
resolve(false);
|
||||
});
|
||||
server.listen(port, () => {
|
||||
server.close(() => {
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
addTools(server, [
|
||||
SelectModeTool,
|
||||
CreateCommentTool,
|
||||
EditCommentTool,
|
||||
CreateWorkingCommentTool,
|
||||
UpdateWorkingCommentTool,
|
||||
IssueTool,
|
||||
PullRequestTool,
|
||||
ReviewTool,
|
||||
PullRequestInfoTool,
|
||||
GetReviewCommentsTool,
|
||||
ListPullRequestReviewsTool,
|
||||
GetCheckSuiteLogsTool,
|
||||
]);
|
||||
let port = startPort;
|
||||
while (port < startPort + 100) {
|
||||
if (await checkPort(port)) {
|
||||
return port;
|
||||
}
|
||||
port++;
|
||||
}
|
||||
throw new Error(`Could not find available port starting from ${startPort}`);
|
||||
}
|
||||
|
||||
server.start();
|
||||
/**
|
||||
* Start the MCP HTTP server and return the URL and close function
|
||||
*/
|
||||
export async function startMcpHttpServer(): Promise<{ url: string; close: () => Promise<void> }> {
|
||||
const server = new FastMCP({
|
||||
name: ghPullfrogMcpName,
|
||||
version: "0.0.1",
|
||||
});
|
||||
|
||||
addTools(server, [
|
||||
SelectModeTool,
|
||||
CreateCommentTool,
|
||||
EditCommentTool,
|
||||
CreateWorkingCommentTool,
|
||||
UpdateWorkingCommentTool,
|
||||
IssueTool,
|
||||
PullRequestTool,
|
||||
ReviewTool,
|
||||
PullRequestInfoTool,
|
||||
GetReviewCommentsTool,
|
||||
ListPullRequestReviewsTool,
|
||||
GetCheckSuiteLogsTool,
|
||||
DebugShellCommandTool,
|
||||
]);
|
||||
|
||||
const port = await findAvailablePort(3764);
|
||||
const host = "127.0.0.1";
|
||||
const endpoint = "/mcp";
|
||||
|
||||
await server.start({
|
||||
transportType: "httpStream",
|
||||
httpStream: {
|
||||
port,
|
||||
host,
|
||||
endpoint,
|
||||
},
|
||||
});
|
||||
|
||||
const url = `http://${host}:${port}${endpoint}`;
|
||||
|
||||
return {
|
||||
url,
|
||||
close: async () => {
|
||||
await server.stop();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
+1
-37
@@ -2,7 +2,6 @@ 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";
|
||||
|
||||
export interface ToolResult {
|
||||
@@ -48,42 +47,7 @@ export interface McpContext extends RepoContext {
|
||||
payload: Payload;
|
||||
}
|
||||
|
||||
export const tool = <const params>(toolDef: Tool<any, StandardSchemaV1<params>>) => {
|
||||
// Wrap the execute function to add logging with the tool name
|
||||
const toolName = toolDef.name;
|
||||
const originalExecute = toolDef.execute;
|
||||
|
||||
toolDef.execute = async (args: params, context: any) => {
|
||||
try {
|
||||
const result = await originalExecute(args, context);
|
||||
|
||||
// TOOL CALL LOGGING DISABLED — now handled by each agent
|
||||
|
||||
// Check if result is a ToolResult with isError property
|
||||
// const isError =
|
||||
// result && typeof result === "object" && "isError" in result && result.isError === true;
|
||||
// const resultData =
|
||||
// result && typeof result === "object" && "content" in result
|
||||
// ? (result as ToolResult).content[0]?.text
|
||||
// : undefined;
|
||||
|
||||
// if (isError && resultData) {
|
||||
// log.toolCall({ toolName, request: args, error: resultData });
|
||||
// } else if (resultData) {
|
||||
// log.toolCall({ toolName, request: args, result: resultData });
|
||||
// } else {
|
||||
// log.toolCall({ toolName, request: args });
|
||||
// }
|
||||
return result;
|
||||
} catch (error) {
|
||||
// const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
// log.toolCall({ toolName, request: args, error: errorMessage });
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return toolDef;
|
||||
};
|
||||
export const tool = <const params>(toolDef: Tool<any, StandardSchemaV1<params>>) => toolDef;
|
||||
|
||||
export const addTools = (server: FastMCP, tools: Tool<any, any>[]) => {
|
||||
for (const tool of tools) {
|
||||
|
||||
@@ -6,7 +6,7 @@ export interface Mode {
|
||||
prompt: string;
|
||||
}
|
||||
|
||||
const initialCommentInstruction = `Use ${ghPullfrogMcpName}/create_working_comment to create an initial Working Comment with a conversational description of what work you are about to perform.`;
|
||||
const initialCommentInstruction = `When the task is associated with an issue/PR, use ${ghPullfrogMcpName}/create_working_comment to create an initial Working Comment with a conversational description of what work you are about to perform.`;
|
||||
|
||||
export const modes: Mode[] = [
|
||||
{
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@pullfrog/action",
|
||||
"version": "0.0.112",
|
||||
"version": "0.0.113",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"index.js",
|
||||
|
||||
@@ -141,6 +141,8 @@ Examples:
|
||||
if (!result.success) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
log.error((err as Error).message);
|
||||
process.exit(1);
|
||||
|
||||
+38
-25
@@ -43,20 +43,22 @@ export function setupTestRepo(options: SetupOptions): void {
|
||||
|
||||
/**
|
||||
* Setup git configuration to avoid identity errors
|
||||
* Only runs in GitHub Actions environment to avoid overwriting local git config
|
||||
* Uses --local flag to scope config to the current repo only
|
||||
*/
|
||||
export function setupGitConfig(): void {
|
||||
// Only set up git config in GitHub Actions environment
|
||||
// In local development, use the user's existing git config
|
||||
if (!process.env.GITHUB_ACTIONS) {
|
||||
return;
|
||||
}
|
||||
|
||||
const repoDir = process.cwd();
|
||||
log.info("🔧 Setting up git configuration...");
|
||||
try {
|
||||
execSync('git config user.email "action@pullfrog.ai"', { stdio: "pipe" });
|
||||
execSync('git config user.name "Pullfrog Action"', { stdio: "pipe" });
|
||||
log.debug("setupGitConfig: ✓ Git configuration set successfully");
|
||||
// Use --local to scope config to this repo only, preventing leakage to user's global config
|
||||
execSync('git config --local user.email "action@pullfrog.ai"', {
|
||||
cwd: repoDir,
|
||||
stdio: "pipe",
|
||||
});
|
||||
execSync('git config --local user.name "Pullfrog Action"', {
|
||||
cwd: repoDir,
|
||||
stdio: "pipe",
|
||||
});
|
||||
log.debug("setupGitConfig: ✓ Git configuration set successfully (scoped to repo)");
|
||||
} catch (error) {
|
||||
// If git config fails, log warning but don't fail the action
|
||||
// This can happen if we're not in a git repo or git isn't available
|
||||
@@ -67,30 +69,34 @@ export function setupGitConfig(): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup git authentication using GitHub token
|
||||
* Only runs in GitHub Actions environment to avoid breaking local git remotes
|
||||
* Setup git authentication using GitHub installation token
|
||||
* Always uses the installation token, scoped to the current repo only
|
||||
*/
|
||||
export function setupGitAuth(githubToken: string, repoContext: RepoContext): void {
|
||||
// Only set up git auth in GitHub Actions environment
|
||||
// In local testing, this would overwrite the real git remote with fake credentials
|
||||
if (!process.env.GITHUB_ACTIONS) {
|
||||
return;
|
||||
}
|
||||
export function setupGitAuth(ctx: {
|
||||
githubInstallationToken: string;
|
||||
repoContext: RepoContext;
|
||||
}): void {
|
||||
const repoDir = process.cwd();
|
||||
|
||||
log.info("🔐 Setting up git authentication...");
|
||||
|
||||
// Remove existing git auth headers that actions/checkout might have set
|
||||
// Use --local to scope to this repo only
|
||||
try {
|
||||
execSync("git config --unset-all http.https://github.com/.extraheader", { stdio: "inherit" });
|
||||
execSync("git config --local --unset-all http.https://github.com/.extraheader", {
|
||||
cwd: repoDir,
|
||||
stdio: "pipe",
|
||||
});
|
||||
log.info("✓ Removed existing authentication headers");
|
||||
} catch {
|
||||
log.info("No existing authentication headers to remove");
|
||||
log.debug("No existing authentication headers to remove");
|
||||
}
|
||||
|
||||
// Update remote URL to embed the token
|
||||
const remoteUrl = `https://x-access-token:${githubToken}@github.com/${repoContext.owner}/${repoContext.name}.git`;
|
||||
$("git", ["remote", "set-url", "origin", remoteUrl]);
|
||||
log.info("✓ Updated remote URL with authentication token");
|
||||
// This is scoped to the repo's .git/config, not the user's global config
|
||||
const remoteUrl = `https://x-access-token:${ctx.githubInstallationToken}@github.com/${ctx.repoContext.owner}/${ctx.repoContext.name}.git`;
|
||||
$("git", ["remote", "set-url", "origin", remoteUrl], { cwd: repoDir });
|
||||
log.info("✓ Updated remote URL with authentication token (scoped to repo)");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,6 +105,7 @@ export function setupGitAuth(githubToken: string, repoContext: RepoContext): voi
|
||||
*/
|
||||
export function setupGitBranch(payload: Payload): void {
|
||||
const branch = payload.event.branch;
|
||||
const repoDir = process.cwd();
|
||||
|
||||
if (!branch) {
|
||||
log.debug("No branch specified in payload, using default branch");
|
||||
@@ -110,11 +117,17 @@ export function setupGitBranch(payload: Payload): void {
|
||||
try {
|
||||
// Fetch the branch from origin
|
||||
log.debug(`Fetching branch from origin: ${branch}`);
|
||||
execSync(`git fetch origin ${branch}`, { stdio: "pipe" });
|
||||
execSync(`git fetch origin ${branch}`, {
|
||||
cwd: repoDir,
|
||||
stdio: "pipe",
|
||||
});
|
||||
|
||||
// Checkout the branch, creating local tracking branch
|
||||
log.debug(`Checking out branch: ${branch}`);
|
||||
execSync(`git checkout -B ${branch} origin/${branch}`, { stdio: "pipe" });
|
||||
execSync(`git checkout -B ${branch} origin/${branch}`, {
|
||||
cwd: repoDir,
|
||||
stdio: "pipe",
|
||||
});
|
||||
|
||||
log.info(`✓ Successfully checked out branch: ${branch}`);
|
||||
} catch (error) {
|
||||
|
||||
+14
-2
@@ -29,8 +29,11 @@ interface ShellOptions {
|
||||
*/
|
||||
export function $(cmd: string, args: string[], options?: ShellOptions): string {
|
||||
const encoding = options?.encoding ?? "utf-8";
|
||||
|
||||
// CRITICAL: use "ignore" for stdin instead of "inherit" to avoid breaking MCP transport
|
||||
// when running inside an MCP server, stdin is used for JSON-RPC protocol
|
||||
const result = spawnSync(cmd, args, {
|
||||
stdio: ["inherit", "pipe", "pipe"],
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
encoding,
|
||||
cwd: options?.cwd,
|
||||
});
|
||||
@@ -39,10 +42,19 @@ export function $(cmd: string, args: string[], options?: ShellOptions): string {
|
||||
const stderr = result.stderr ?? "";
|
||||
|
||||
// Write output to process streams so it behaves like stdio: "inherit"
|
||||
// CRITICAL: when running inside an MCP server, stdout is used for JSON-RPC protocol
|
||||
// so we must write to stderr instead to avoid corrupting the protocol
|
||||
// Only log if log option is not explicitly set to false
|
||||
if (options?.log !== false) {
|
||||
// if stdout is a TTY, it's safe to write to it; otherwise it's likely a pipe used for JSON-RPC
|
||||
const canWriteToStdout = process.stdout.isTTY === true;
|
||||
if (stdout) {
|
||||
process.stdout.write(stdout);
|
||||
if (canWriteToStdout) {
|
||||
process.stdout.write(stdout);
|
||||
} else {
|
||||
// stdout is a pipe (MCP context) - write to stderr instead
|
||||
process.stderr.write(stdout);
|
||||
}
|
||||
}
|
||||
if (stderr) {
|
||||
process.stderr.write(stderr);
|
||||
|
||||
Reference in New Issue
Block a user