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
|
pnpm build
|
||||||
|
|
||||||
# Add the built files and lockfile to the commit
|
# Add the built files and lockfile to the commit
|
||||||
git add entry mcp-server pnpm-lock.yaml
|
git add entry pnpm-lock.yaml
|
||||||
fi
|
fi
|
||||||
|
|||||||
+19
-23
@@ -128,7 +128,7 @@ const messageHandlers: {
|
|||||||
toolName: item.tool,
|
toolName: item.tool,
|
||||||
input: {
|
input: {
|
||||||
server: item.server,
|
server: item.server,
|
||||||
...((item as any).args || {}),
|
...((item as any).arguments || {}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -167,7 +167,7 @@ const messageHandlers: {
|
|||||||
const reasoningText = item.text.trim();
|
const reasoningText = item.text.trim();
|
||||||
// Remove markdown bold markers if present for cleaner output
|
// Remove markdown bold markers if present for cleaner output
|
||||||
const cleanText = reasoningText.replace(/\*\*/g, "");
|
const cleanText = reasoningText.replace(/\*\*/g, "");
|
||||||
log.info(cleanText);
|
log.box(cleanText, { title: "Codex" });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (event) => {
|
error: (event) => {
|
||||||
@@ -177,34 +177,30 @@ const messageHandlers: {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Configure MCP servers for Codex using the CLI.
|
* 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 {
|
function configureCodexMcpServers({ mcpServers, cliPath }: ConfigureMcpServersParams): void {
|
||||||
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
||||||
const command = serverConfig.command;
|
if (serverConfig.type === "http") {
|
||||||
const args = serverConfig.args || [];
|
// HTTP-based MCP server - use --url flag
|
||||||
const envVars = serverConfig.env || {};
|
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
|
if (addResult.status !== 0) {
|
||||||
for (const [key, value] of Object.entries(envVars)) {
|
throw new Error(
|
||||||
addArgs.push("--env", `${key}=${value}`);
|
`codex mcp add failed: ${addResult.stderr || addResult.stdout || "Unknown error"}`
|
||||||
}
|
);
|
||||||
|
}
|
||||||
addArgs.push("--", command, ...args);
|
log.info(`✓ MCP server '${serverName}' configured`);
|
||||||
|
} else {
|
||||||
log.info(`Adding MCP server '${serverName}'...`);
|
|
||||||
const addResult = spawnSync("node", [cliPath, ...addArgs], {
|
|
||||||
stdio: "pipe",
|
|
||||||
encoding: "utf-8",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (addResult.status !== 0) {
|
|
||||||
throw new Error(
|
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 cursorConfigDir = join(realHome, ".cursor");
|
||||||
const mcpConfigPath = join(cursorConfigDir, "mcp.json");
|
const mcpConfigPath = join(cursorConfigDir, "mcp.json");
|
||||||
mkdirSync(cursorConfigDir, { recursive: true });
|
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}`);
|
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.
|
* 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 {
|
function configureGeminiMcpServers({ mcpServers, cliPath }: ConfigureMcpServersParams): void {
|
||||||
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
||||||
const command = serverConfig.command;
|
if (serverConfig.type === "http") {
|
||||||
const args = serverConfig.args || [];
|
// HTTP-based MCP server - use URL with --transport http flag
|
||||||
const envVars = serverConfig.env || {};
|
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)) {
|
if (addResult.status !== 0) {
|
||||||
addArgs.push("--env", `${key}=${value}`);
|
throw new Error(
|
||||||
}
|
`gemini mcp add failed: ${addResult.stderr || addResult.stdout || "Unknown error"}`
|
||||||
|
);
|
||||||
log.info(`Adding MCP server '${serverName}'...`);
|
}
|
||||||
const addResult = spawnSync("node", [cliPath, ...addArgs], {
|
log.info(`✓ MCP server '${serverName}' configured`);
|
||||||
stdio: "pipe",
|
} else {
|
||||||
encoding: "utf-8",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (addResult.status !== 0) {
|
|
||||||
throw new Error(
|
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 { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { pipeline } from "node:stream/promises";
|
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 { show } from "@ark/util";
|
||||||
import { type AgentManifest, type AgentName, agentsManifest, type Payload } from "../external.ts";
|
import { type AgentManifest, type AgentName, agentsManifest, type Payload } from "../external.ts";
|
||||||
import { log } from "../utils/cli.ts";
|
import { log } from "../utils/cli.ts";
|
||||||
@@ -26,7 +26,7 @@ export interface AgentConfig {
|
|||||||
apiKey: string;
|
apiKey: string;
|
||||||
githubInstallationToken: string;
|
githubInstallationToken: string;
|
||||||
payload: Payload;
|
payload: Payload;
|
||||||
mcpServers: Record<string, McpStdioServerConfig>;
|
mcpServers: Record<string, McpHttpServerConfig>;
|
||||||
cliPath: string;
|
cliPath: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ export interface AgentConfig {
|
|||||||
* Parameters for configuring MCP servers
|
* Parameters for configuring MCP servers
|
||||||
*/
|
*/
|
||||||
export interface ConfigureMcpServersParams {
|
export interface ConfigureMcpServersParams {
|
||||||
mcpServers: Record<string, McpStdioServerConfig>;
|
mcpServers: Record<string, McpHttpServerConfig>;
|
||||||
cliPath: string;
|
cliPath: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-9
@@ -59,7 +59,7 @@ const sharedConfig = {
|
|||||||
drop: [],
|
drop: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build the main entry bundle (without MCP)
|
// Build the main entry bundle
|
||||||
await build({
|
await build({
|
||||||
...sharedConfig,
|
...sharedConfig,
|
||||||
entryPoints: ["./entry.ts"],
|
entryPoints: ["./entry.ts"],
|
||||||
@@ -67,12 +67,4 @@ await build({
|
|||||||
plugins: [stripShebangPlugin],
|
plugins: [stripShebangPlugin],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Build the MCP server bundle
|
|
||||||
await build({
|
|
||||||
...sharedConfig,
|
|
||||||
entryPoints: ["./mcp/server.ts"],
|
|
||||||
outfile: "./mcp-server",
|
|
||||||
plugins: [stripShebangPlugin],
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("✅ Build completed successfully!");
|
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 type { AgentName, AgentName as AgentNameType, Payload } from "./external.ts";
|
||||||
import { agentsManifest } from "./external.ts";
|
import { agentsManifest } from "./external.ts";
|
||||||
import { createMcpConfigs } from "./mcp/config.ts";
|
import { createMcpConfigs } from "./mcp/config.ts";
|
||||||
|
import { startMcpHttpServer } from "./mcp/server.ts";
|
||||||
import { modes } from "./modes.ts";
|
import { modes } from "./modes.ts";
|
||||||
import packageJson from "./package.json" with { type: "json" };
|
import packageJson from "./package.json" with { type: "json" };
|
||||||
import { fetchRepoSettings } from "./utils/api.ts";
|
import { fetchRepoSettings } from "./utils/api.ts";
|
||||||
@@ -48,39 +49,29 @@ export interface MainResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function main(inputs: Inputs): Promise<MainResult> {
|
export async function main(inputs: Inputs): Promise<MainResult> {
|
||||||
let githubInstallationToken: string | undefined;
|
|
||||||
let pollInterval: NodeJS.Timeout | null = null;
|
let pollInterval: NodeJS.Timeout | null = null;
|
||||||
|
let mcpServerClose: (() => Promise<void>) | undefined;
|
||||||
|
let githubInstallationToken: string | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// parse payload early to extract agent
|
// parse payload early to extract agent
|
||||||
const payload = parsePayload(inputs);
|
const payload = parsePayload(inputs);
|
||||||
|
|
||||||
// resolve agent before initializing context
|
const partialCtx = await initializeContext(inputs, payload);
|
||||||
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 ctx = partialCtx as MainContext;
|
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);
|
await setupTempDirectory(ctx);
|
||||||
setupMcpLogPolling(ctx);
|
setupMcpLogPolling(ctx);
|
||||||
pollInterval = ctx.pollInterval;
|
pollInterval = ctx.pollInterval;
|
||||||
|
|
||||||
setupGitBranch(ctx.payload);
|
setupGitBranch(ctx.payload);
|
||||||
|
await startMcpServer(ctx);
|
||||||
|
mcpServerClose = ctx.mcpServerClose;
|
||||||
setupMcpServers(ctx);
|
setupMcpServers(ctx);
|
||||||
await installAgentCli(ctx);
|
await installAgentCli(ctx);
|
||||||
validateApiKey(ctx);
|
validateApiKey(ctx);
|
||||||
@@ -99,6 +90,9 @@ export async function main(inputs: Inputs): Promise<MainResult> {
|
|||||||
if (pollInterval) {
|
if (pollInterval) {
|
||||||
clearInterval(pollInterval);
|
clearInterval(pollInterval);
|
||||||
}
|
}
|
||||||
|
if (mcpServerClose) {
|
||||||
|
await mcpServerClose();
|
||||||
|
}
|
||||||
if (githubInstallationToken) {
|
if (githubInstallationToken) {
|
||||||
await revokeInstallationToken(githubInstallationToken);
|
await revokeInstallationToken(githubInstallationToken);
|
||||||
}
|
}
|
||||||
@@ -179,6 +173,8 @@ interface MainContext {
|
|||||||
mcpLogPath: string;
|
mcpLogPath: string;
|
||||||
pollInterval: NodeJS.Timeout | null;
|
pollInterval: NodeJS.Timeout | null;
|
||||||
payload: Payload;
|
payload: Payload;
|
||||||
|
mcpServerUrl: string;
|
||||||
|
mcpServerClose: () => Promise<void>;
|
||||||
mcpServers: ReturnType<typeof createMcpConfigs>;
|
mcpServers: ReturnType<typeof createMcpConfigs>;
|
||||||
cliPath: string;
|
cliPath: string;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
@@ -186,21 +182,33 @@ interface MainContext {
|
|||||||
|
|
||||||
async function initializeContext(
|
async function initializeContext(
|
||||||
inputs: Inputs,
|
inputs: Inputs,
|
||||||
agentName: AgentNameType,
|
payload: Payload
|
||||||
agent: (typeof agents)[AgentNameType],
|
): Promise<
|
||||||
githubInstallationToken: string,
|
Omit<MainContext, "mcpServerUrl" | "mcpServerClose" | "mcpServers" | "cliPath" | "apiKey">
|
||||||
repoContext: RepoContext
|
> {
|
||||||
): Promise<Omit<MainContext, "payload" | "mcpServers" | "cliPath" | "apiKey">> {
|
|
||||||
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
|
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
|
||||||
Inputs.assert(inputs);
|
Inputs.assert(inputs);
|
||||||
setupGitConfig();
|
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 {
|
return {
|
||||||
inputs,
|
inputs,
|
||||||
githubInstallationToken,
|
githubInstallationToken,
|
||||||
repoContext,
|
repoContext,
|
||||||
agentName,
|
agentName,
|
||||||
agent,
|
agent,
|
||||||
|
payload: resolvedPayload,
|
||||||
sharedTempDir: "",
|
sharedTempDir: "",
|
||||||
mcpLogPath: "",
|
mcpLogPath: "",
|
||||||
pollInterval: null,
|
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 || [])];
|
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)}`);
|
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 agentDisplayName = agentInfo?.displayName || "Unknown Agent";
|
||||||
const agentUrl = agentInfo?.url || "https://pullfrog.ai";
|
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
|
const workflowRunUrl = runId
|
||||||
? `https://github.com/${repoContext.owner}/${repoContext.name}/actions/runs/${runId}`
|
? `https://github.com/${repoContext.owner}/${repoContext.name}/actions/runs/${runId}`
|
||||||
: `https://github.com/${repoContext.owner}/${repoContext.name}`;
|
: `https://github.com/${repoContext.owner}/${repoContext.name}`;
|
||||||
|
|||||||
+5
-34
@@ -2,47 +2,18 @@
|
|||||||
* Simple MCP configuration helper for adding our minimal GitHub comment server
|
* Simple MCP configuration helper for adding our minimal GitHub comment server
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
import type { McpHttpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
||||||
import { fromHere } from "@ark/fs";
|
|
||||||
import type { Payload } from "../external.ts";
|
|
||||||
import { ghPullfrogMcpName } from "../external.ts";
|
import { ghPullfrogMcpName } from "../external.ts";
|
||||||
import type { Mode } from "../modes.ts";
|
|
||||||
import { parseRepoContext } from "../utils/github.ts";
|
|
||||||
|
|
||||||
export type McpName = typeof ghPullfrogMcpName;
|
export type McpName = typeof ghPullfrogMcpName;
|
||||||
|
|
||||||
export type McpConfigs = Record<McpName, McpStdioServerConfig>;
|
export type McpConfigs = Record<McpName, McpHttpServerConfig>;
|
||||||
|
|
||||||
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 function createMcpConfigs(mcpServerUrl: string): McpConfigs {
|
||||||
return {
|
return {
|
||||||
[ghPullfrogMcpName]: {
|
[ghPullfrogMcpName]: {
|
||||||
command: "node",
|
type: "http",
|
||||||
args: [serverPath],
|
url: mcpServerUrl,
|
||||||
env,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
import "./arkConfig.ts";
|
||||||
// this must be imported first
|
// this must be imported first
|
||||||
|
import { createServer } from "node:net";
|
||||||
import { FastMCP } from "fastmcp";
|
import { FastMCP } from "fastmcp";
|
||||||
import { ghPullfrogMcpName } from "../external.ts";
|
import { ghPullfrogMcpName } from "../external.ts";
|
||||||
import { GetCheckSuiteLogsTool } from "./checkSuite.ts";
|
import { GetCheckSuiteLogsTool } from "./checkSuite.ts";
|
||||||
@@ -9,6 +10,7 @@ import {
|
|||||||
EditCommentTool,
|
EditCommentTool,
|
||||||
UpdateWorkingCommentTool,
|
UpdateWorkingCommentTool,
|
||||||
} from "./comment.ts";
|
} from "./comment.ts";
|
||||||
|
import { DebugShellCommandTool } from "./debug.ts";
|
||||||
import { IssueTool } from "./issue.ts";
|
import { IssueTool } from "./issue.ts";
|
||||||
import { PullRequestTool } from "./pr.ts";
|
import { PullRequestTool } from "./pr.ts";
|
||||||
import { PullRequestInfoTool } from "./prInfo.ts";
|
import { PullRequestInfoTool } from "./prInfo.ts";
|
||||||
@@ -17,24 +19,79 @@ import { GetReviewCommentsTool, ListPullRequestReviewsTool } from "./reviewComme
|
|||||||
import { SelectModeTool } from "./selectMode.ts";
|
import { SelectModeTool } from "./selectMode.ts";
|
||||||
import { addTools } from "./shared.ts";
|
import { addTools } from "./shared.ts";
|
||||||
|
|
||||||
const server = new FastMCP({
|
/**
|
||||||
name: ghPullfrogMcpName,
|
* Find an available port starting from the given port
|
||||||
version: "0.0.1",
|
*/
|
||||||
});
|
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, [
|
let port = startPort;
|
||||||
SelectModeTool,
|
while (port < startPort + 100) {
|
||||||
CreateCommentTool,
|
if (await checkPort(port)) {
|
||||||
EditCommentTool,
|
return port;
|
||||||
CreateWorkingCommentTool,
|
}
|
||||||
UpdateWorkingCommentTool,
|
port++;
|
||||||
IssueTool,
|
}
|
||||||
PullRequestTool,
|
throw new Error(`Could not find available port starting from ${startPort}`);
|
||||||
ReviewTool,
|
}
|
||||||
PullRequestInfoTool,
|
|
||||||
GetReviewCommentsTool,
|
|
||||||
ListPullRequestReviewsTool,
|
|
||||||
GetCheckSuiteLogsTool,
|
|
||||||
]);
|
|
||||||
|
|
||||||
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 { StandardSchemaV1 } from "@standard-schema/spec";
|
||||||
import type { FastMCP, Tool } from "fastmcp";
|
import type { FastMCP, Tool } from "fastmcp";
|
||||||
import type { Payload } from "../external.ts";
|
import type { Payload } from "../external.ts";
|
||||||
import { log } from "../utils/cli.ts";
|
|
||||||
import { parseRepoContext, type RepoContext } from "../utils/github.ts";
|
import { parseRepoContext, type RepoContext } from "../utils/github.ts";
|
||||||
|
|
||||||
export interface ToolResult {
|
export interface ToolResult {
|
||||||
@@ -48,42 +47,7 @@ export interface McpContext extends RepoContext {
|
|||||||
payload: Payload;
|
payload: Payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const tool = <const params>(toolDef: Tool<any, StandardSchemaV1<params>>) => {
|
export const tool = <const params>(toolDef: Tool<any, StandardSchemaV1<params>>) => toolDef;
|
||||||
// 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 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) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export interface Mode {
|
|||||||
prompt: string;
|
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[] = [
|
export const modes: Mode[] = [
|
||||||
{
|
{
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@pullfrog/action",
|
"name": "@pullfrog/action",
|
||||||
"version": "0.0.112",
|
"version": "0.0.113",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"index.js",
|
"index.js",
|
||||||
|
|||||||
@@ -141,6 +141,8 @@ Examples:
|
|||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error((err as Error).message);
|
log.error((err as Error).message);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
+38
-25
@@ -43,20 +43,22 @@ export function setupTestRepo(options: SetupOptions): void {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup git configuration to avoid identity errors
|
* 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 {
|
export function setupGitConfig(): void {
|
||||||
// Only set up git config in GitHub Actions environment
|
const repoDir = process.cwd();
|
||||||
// In local development, use the user's existing git config
|
|
||||||
if (!process.env.GITHUB_ACTIONS) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("🔧 Setting up git configuration...");
|
log.info("🔧 Setting up git configuration...");
|
||||||
try {
|
try {
|
||||||
execSync('git config user.email "action@pullfrog.ai"', { stdio: "pipe" });
|
// Use --local to scope config to this repo only, preventing leakage to user's global config
|
||||||
execSync('git config user.name "Pullfrog Action"', { stdio: "pipe" });
|
execSync('git config --local user.email "action@pullfrog.ai"', {
|
||||||
log.debug("setupGitConfig: ✓ Git configuration set successfully");
|
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) {
|
} catch (error) {
|
||||||
// If git config fails, log warning but don't fail the action
|
// 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
|
// 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
|
* Setup git authentication using GitHub installation token
|
||||||
* Only runs in GitHub Actions environment to avoid breaking local git remotes
|
* Always uses the installation token, scoped to the current repo only
|
||||||
*/
|
*/
|
||||||
export function setupGitAuth(githubToken: string, repoContext: RepoContext): void {
|
export function setupGitAuth(ctx: {
|
||||||
// Only set up git auth in GitHub Actions environment
|
githubInstallationToken: string;
|
||||||
// In local testing, this would overwrite the real git remote with fake credentials
|
repoContext: RepoContext;
|
||||||
if (!process.env.GITHUB_ACTIONS) {
|
}): void {
|
||||||
return;
|
const repoDir = process.cwd();
|
||||||
}
|
|
||||||
|
|
||||||
log.info("🔐 Setting up git authentication...");
|
log.info("🔐 Setting up git authentication...");
|
||||||
|
|
||||||
// Remove existing git auth headers that actions/checkout might have set
|
// Remove existing git auth headers that actions/checkout might have set
|
||||||
|
// Use --local to scope to this repo only
|
||||||
try {
|
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");
|
log.info("✓ Removed existing authentication headers");
|
||||||
} catch {
|
} catch {
|
||||||
log.info("No existing authentication headers to remove");
|
log.debug("No existing authentication headers to remove");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update remote URL to embed the token
|
// Update remote URL to embed the token
|
||||||
const remoteUrl = `https://x-access-token:${githubToken}@github.com/${repoContext.owner}/${repoContext.name}.git`;
|
// This is scoped to the repo's .git/config, not the user's global config
|
||||||
$("git", ["remote", "set-url", "origin", remoteUrl]);
|
const remoteUrl = `https://x-access-token:${ctx.githubInstallationToken}@github.com/${ctx.repoContext.owner}/${ctx.repoContext.name}.git`;
|
||||||
log.info("✓ Updated remote URL with authentication token");
|
$("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 {
|
export function setupGitBranch(payload: Payload): void {
|
||||||
const branch = payload.event.branch;
|
const branch = payload.event.branch;
|
||||||
|
const repoDir = process.cwd();
|
||||||
|
|
||||||
if (!branch) {
|
if (!branch) {
|
||||||
log.debug("No branch specified in payload, using default branch");
|
log.debug("No branch specified in payload, using default branch");
|
||||||
@@ -110,11 +117,17 @@ export function setupGitBranch(payload: Payload): void {
|
|||||||
try {
|
try {
|
||||||
// Fetch the branch from origin
|
// Fetch the branch from origin
|
||||||
log.debug(`Fetching branch from origin: ${branch}`);
|
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
|
// Checkout the branch, creating local tracking branch
|
||||||
log.debug(`Checking out branch: ${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}`);
|
log.info(`✓ Successfully checked out branch: ${branch}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
+14
-2
@@ -29,8 +29,11 @@ interface ShellOptions {
|
|||||||
*/
|
*/
|
||||||
export function $(cmd: string, args: string[], options?: ShellOptions): string {
|
export function $(cmd: string, args: string[], options?: ShellOptions): string {
|
||||||
const encoding = options?.encoding ?? "utf-8";
|
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, {
|
const result = spawnSync(cmd, args, {
|
||||||
stdio: ["inherit", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
encoding,
|
encoding,
|
||||||
cwd: options?.cwd,
|
cwd: options?.cwd,
|
||||||
});
|
});
|
||||||
@@ -39,10 +42,19 @@ export function $(cmd: string, args: string[], options?: ShellOptions): string {
|
|||||||
const stderr = result.stderr ?? "";
|
const stderr = result.stderr ?? "";
|
||||||
|
|
||||||
// Write output to process streams so it behaves like stdio: "inherit"
|
// 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
|
// Only log if log option is not explicitly set to false
|
||||||
if (options?.log !== 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) {
|
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) {
|
if (stderr) {
|
||||||
process.stderr.write(stderr);
|
process.stderr.write(stderr);
|
||||||
|
|||||||
Reference in New Issue
Block a user