switch to http mcp
This commit is contained in:
+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
|
||||
|
||||
+17
-21
@@ -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!");
|
||||
|
||||
@@ -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";
|
||||
@@ -17,6 +18,7 @@ import { log } from "./utils/cli.ts";
|
||||
import {
|
||||
parseRepoContext,
|
||||
type RepoContext,
|
||||
revokeInstallationToken,
|
||||
setupGitHubInstallationToken,
|
||||
} from "./utils/github.ts";
|
||||
import { setupGitAuth, setupGitBranch, setupGitConfig } from "./utils/setup.ts";
|
||||
@@ -48,6 +50,8 @@ export interface MainResult {
|
||||
|
||||
export async function main(inputs: Inputs): Promise<MainResult> {
|
||||
let pollInterval: NodeJS.Timeout | null = null;
|
||||
let mcpServerClose: (() => Promise<void>) | undefined;
|
||||
let githubInstallationToken: string | undefined;
|
||||
|
||||
try {
|
||||
// parse payload early to extract agent
|
||||
@@ -55,13 +59,19 @@ export async function main(inputs: Inputs): Promise<MainResult> {
|
||||
|
||||
const partialCtx = await initializeContext(inputs, payload);
|
||||
const ctx = partialCtx as MainContext;
|
||||
githubInstallationToken = ctx.githubInstallationToken;
|
||||
|
||||
setupGitAuth(ctx);
|
||||
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);
|
||||
@@ -80,6 +90,12 @@ export async function main(inputs: Inputs): Promise<MainResult> {
|
||||
if (pollInterval) {
|
||||
clearInterval(pollInterval);
|
||||
}
|
||||
if (mcpServerClose) {
|
||||
await mcpServerClose();
|
||||
}
|
||||
if (githubInstallationToken) {
|
||||
await revokeInstallationToken(githubInstallationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,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;
|
||||
@@ -165,7 +183,9 @@ interface MainContext {
|
||||
async function initializeContext(
|
||||
inputs: Inputs,
|
||||
payload: Payload
|
||||
): Promise<Omit<MainContext, "mcpServers" | "cliPath" | "apiKey">> {
|
||||
): Promise<
|
||||
Omit<MainContext, "mcpServerUrl" | "mcpServerClose" | "mcpServers" | "cliPath" | "apiKey">
|
||||
> {
|
||||
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
|
||||
Inputs.assert(inputs);
|
||||
setupGitConfig();
|
||||
@@ -281,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
+5
-35
@@ -2,48 +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)
|
||||
// this is used to build the "View workflow run" link in comments
|
||||
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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
+75
-20
@@ -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";
|
||||
@@ -18,25 +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,
|
||||
DebugShellCommandTool,
|
||||
]);
|
||||
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
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@pullfrog/action",
|
||||
"version": "0.0.112",
|
||||
"version": "0.0.113",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"index.js",
|
||||
|
||||
Reference in New Issue
Block a user