From d1f16e9dd2633ca5763090c13e879cc2da17c54d Mon Sep 17 00:00:00 2001 From: Shawn Morreau Date: Fri, 14 Nov 2025 14:27:40 -0500 Subject: [PATCH 01/10] begin cursor --- agents/cursor.ts | 184 +++++++++++++++++++++++++++++++++++++++++++++++ agents/index.ts | 2 + play.ts | 2 +- 3 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 agents/cursor.ts diff --git a/agents/cursor.ts b/agents/cursor.ts new file mode 100644 index 0000000..bf737be --- /dev/null +++ b/agents/cursor.ts @@ -0,0 +1,184 @@ +import { spawnSync, spawn } from "node:child_process"; +import { chmodSync, createWriteStream, existsSync } from "node:fs"; +import { mkdtemp } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { pipeline } from "node:stream/promises"; +import { log } from "../utils/cli.ts"; +import { addInstructions } from "./instructions.ts"; +import { agent } from "./shared.ts"; + +/** + * Install Cursor CLI to a temporary directory + * Downloads the install script and runs it with PREFIX set to temp directory + * Falls back to checking system PATH if installation fails + */ +async function installCursorCli(): Promise { + log.info("📦 Installing Cursor CLI..."); + + // Create temp directory + const tempDir = await mkdtemp(join(tmpdir(), "cursor-cli-")); + const installScriptPath = join(tempDir, "install.sh"); + const binDir = join(tempDir, "bin"); + + // Download the install script + log.info("Downloading Cursor CLI install script..."); + const installScriptResponse = await fetch("https://cursor.com/install"); + if (!installScriptResponse.ok) { + throw new Error(`Failed to download install script: ${installScriptResponse.status}`); + } + + if (!installScriptResponse.body) throw new Error("Response body is null"); + const fileStream = createWriteStream(installScriptPath); + await pipeline(installScriptResponse.body, fileStream); + + // Make install script executable + chmodSync(installScriptPath, 0o755); + + log.info("Installing Cursor CLI to temp directory..."); + + // Try to run the install script with PREFIX set to temp directory + // Many install scripts respect PREFIX or INSTALL_PREFIX environment variables + const installResult = spawnSync("bash", [installScriptPath], { + cwd: tempDir, + env: { + ...process.env, + PREFIX: tempDir, + INSTALL_PREFIX: tempDir, + DESTDIR: tempDir, + HOME: tempDir, // Some scripts use HOME for user-specific installs + }, + stdio: "pipe", + encoding: "utf-8", + }); + + // Check common installation locations + const possiblePaths = [ + join(binDir, "cursor-agent"), + join(tempDir, "cursor-agent"), + join(tempDir, ".local", "bin", "cursor-agent"), + join(tempDir, ".cursor", "bin", "cursor-agent"), + ]; + + let cliPath: string | null = null; + for (const path of possiblePaths) { + if (existsSync(path)) { + cliPath = path; + break; + } + } + + // If not found, check if cursor-agent is already in PATH (fallback) + if (!cliPath) { + const whichResult = spawnSync("which", ["cursor-agent"], { + stdio: "pipe", + encoding: "utf-8", + }); + if (whichResult.status === 0 && whichResult.stdout) { + cliPath = whichResult.stdout.trim(); + log.info(`Using system cursor-agent at ${cliPath}`); + } + } + + if (!cliPath || !existsSync(cliPath)) { + // Provide helpful error message + const errorOutput = installResult.stderr || installResult.stdout || "No output"; + throw new Error( + `Failed to install Cursor CLI. Install script exited with code ${installResult.status}. Output: ${errorOutput}` + ); + } + + // Ensure binary is executable + chmodSync(cliPath, 0o755); + log.info(`✓ Cursor CLI installed at ${cliPath}`); + return cliPath; +} + +export const cursor = agent({ + name: "cursor", + inputKey: "cursor_api_key", + install: installCursorCli, + run: async ({ prompt, mcpServers, apiKey, cliPath, githubInstallationToken }) => { + process.env.CURSOR_API_KEY = apiKey; + process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken; + + // TODO: Configure MCP servers for Cursor if supported + // Cursor CLI may support MCP configuration similar to Codex + if (mcpServers && Object.keys(mcpServers).length > 0) { + log.info("MCP server configuration for Cursor CLI is not yet implemented"); + } + + try { + // Run cursor-agent in non-interactive mode with the prompt + // Using -p flag for prompt and --output-format text for plain text output + const fullPrompt = addInstructions(prompt); + + log.info("Running Cursor CLI..."); + + // Use spawn to handle streaming output + return new Promise((resolve) => { + const child = spawn(cliPath, ["-p", fullPrompt, "--output-format", "text"], { + env: { + ...process.env, + CURSOR_API_KEY: apiKey, + GITHUB_INSTALLATION_TOKEN: githubInstallationToken, + }, + stdio: ["pipe", "pipe", "pipe"], + }); + + let stdout = ""; + let stderr = ""; + + child.stdout?.on("data", (data) => { + const text = data.toString(); + stdout += text; + // Stream output in real-time + process.stdout.write(text); + }); + + child.stderr?.on("data", (data) => { + const text = data.toString(); + stderr += text; + // Log errors as they come + log.warning(text); + }); + + child.on("close", (code) => { + if (code === 0) { + log.success("Cursor CLI completed successfully"); + resolve({ + success: true, + output: stdout.trim(), + }); + } else { + const errorMessage = stderr || `Cursor CLI exited with code ${code}`; + log.error(`Cursor CLI failed: ${errorMessage}`); + resolve({ + success: false, + error: errorMessage, + output: stdout.trim(), + }); + } + }); + + child.on("error", (error) => { + const errorMessage = error.message || String(error); + log.error(`Cursor CLI execution failed: ${errorMessage}`); + resolve({ + success: false, + error: errorMessage, + output: stdout.trim(), + }); + }); + }); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + log.error(`Cursor execution failed: ${errorMessage}`); + return { + success: false, + error: errorMessage, + output: "", + }; + } + }, +}); diff --git a/agents/index.ts b/agents/index.ts index 553aadf..a1c439c 100644 --- a/agents/index.ts +++ b/agents/index.ts @@ -1,9 +1,11 @@ import { claude } from "./claude.ts"; import { codex } from "./codex.ts"; +import { cursor } from "./cursor.ts"; export const agents = { claude, codex, + cursor, } as const; export type AgentInputKey = (typeof agents)[keyof typeof agents]["inputKey"]; diff --git a/play.ts b/play.ts index ba19f3a..3943be0 100644 --- a/play.ts +++ b/play.ts @@ -24,7 +24,7 @@ export async function run( prompt, openai_api_key: process.env.OPENAI_API_KEY, anthropic_api_key: process.env.ANTHROPIC_API_KEY, - agent: "codex", + agent: "cursor", }; const result = await main(inputs); From b2a9b60271c1e58ca9438b7c50874cd08435aa2f Mon Sep 17 00:00:00 2001 From: Shawn Morreau Date: Fri, 14 Nov 2025 16:03:19 -0500 Subject: [PATCH 02/10] first iteration of pnpm play working --- agents/cursor.ts | 221 +++++++++++++++++++++++---------------------- agents/shared.ts | 74 +++++++++++++++ fixtures/basic.txt | 2 +- play.ts | 2 +- 4 files changed, 190 insertions(+), 109 deletions(-) diff --git a/agents/cursor.ts b/agents/cursor.ts index bf737be..1f1808a 100644 --- a/agents/cursor.ts +++ b/agents/cursor.ts @@ -1,135 +1,82 @@ -import { spawnSync, spawn } from "node:child_process"; -import { chmodSync, createWriteStream, existsSync } from "node:fs"; -import { mkdtemp } from "node:fs/promises"; -import { tmpdir } from "node:os"; +import { spawn } from "node:child_process"; +import { mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; -import { pipeline } from "node:stream/promises"; +import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk"; import { log } from "../utils/cli.ts"; import { addInstructions } from "./instructions.ts"; -import { agent } from "./shared.ts"; - -/** - * Install Cursor CLI to a temporary directory - * Downloads the install script and runs it with PREFIX set to temp directory - * Falls back to checking system PATH if installation fails - */ -async function installCursorCli(): Promise { - log.info("📦 Installing Cursor CLI..."); - - // Create temp directory - const tempDir = await mkdtemp(join(tmpdir(), "cursor-cli-")); - const installScriptPath = join(tempDir, "install.sh"); - const binDir = join(tempDir, "bin"); - - // Download the install script - log.info("Downloading Cursor CLI install script..."); - const installScriptResponse = await fetch("https://cursor.com/install"); - if (!installScriptResponse.ok) { - throw new Error(`Failed to download install script: ${installScriptResponse.status}`); - } - - if (!installScriptResponse.body) throw new Error("Response body is null"); - const fileStream = createWriteStream(installScriptPath); - await pipeline(installScriptResponse.body, fileStream); - - // Make install script executable - chmodSync(installScriptPath, 0o755); - - log.info("Installing Cursor CLI to temp directory..."); - - // Try to run the install script with PREFIX set to temp directory - // Many install scripts respect PREFIX or INSTALL_PREFIX environment variables - const installResult = spawnSync("bash", [installScriptPath], { - cwd: tempDir, - env: { - ...process.env, - PREFIX: tempDir, - INSTALL_PREFIX: tempDir, - DESTDIR: tempDir, - HOME: tempDir, // Some scripts use HOME for user-specific installs - }, - stdio: "pipe", - encoding: "utf-8", - }); - - // Check common installation locations - const possiblePaths = [ - join(binDir, "cursor-agent"), - join(tempDir, "cursor-agent"), - join(tempDir, ".local", "bin", "cursor-agent"), - join(tempDir, ".cursor", "bin", "cursor-agent"), - ]; - - let cliPath: string | null = null; - for (const path of possiblePaths) { - if (existsSync(path)) { - cliPath = path; - break; - } - } - - // If not found, check if cursor-agent is already in PATH (fallback) - if (!cliPath) { - const whichResult = spawnSync("which", ["cursor-agent"], { - stdio: "pipe", - encoding: "utf-8", - }); - if (whichResult.status === 0 && whichResult.stdout) { - cliPath = whichResult.stdout.trim(); - log.info(`Using system cursor-agent at ${cliPath}`); - } - } - - if (!cliPath || !existsSync(cliPath)) { - // Provide helpful error message - const errorOutput = installResult.stderr || installResult.stdout || "No output"; - throw new Error( - `Failed to install Cursor CLI. Install script exited with code ${installResult.status}. Output: ${errorOutput}` - ); - } - - // Ensure binary is executable - chmodSync(cliPath, 0o755); - log.info(`✓ Cursor CLI installed at ${cliPath}`); - return cliPath; -} +import { agent, installFromCurl } from "./shared.ts"; export const cursor = agent({ name: "cursor", inputKey: "cursor_api_key", - install: installCursorCli, + install: async () => { + return await installFromCurl({ + installUrl: "https://cursor.com/install", + executableName: "cursor-agent", + }); + }, run: async ({ prompt, mcpServers, apiKey, cliPath, githubInstallationToken }) => { process.env.CURSOR_API_KEY = apiKey; process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken; - // TODO: Configure MCP servers for Cursor if supported - // Cursor CLI may support MCP configuration similar to Codex + // Configure MCP servers for Cursor (global config is fine - not part of repo) if (mcpServers && Object.keys(mcpServers).length > 0) { - log.info("MCP server configuration for Cursor CLI is not yet implemented"); + configureMcpServers({ mcpServers, cliPath }); } try { // Run cursor-agent in non-interactive mode with the prompt - // Using -p flag for prompt and --output-format text for plain text output + // Using -p flag for prompt, --output-format text for plain text output + // and --approve-mcps to automatically approve all MCP servers const fullPrompt = addInstructions(prompt); + // Find temp directory from cliPath to set HOME for MCP config lookup + const tempDir = cliPath.split("/.local/bin/")[0]; + log.info("Running Cursor CLI..."); // Use spawn to handle streaming output + // Use --print flag explicitly for non-interactive mode return new Promise((resolve) => { - const child = spawn(cliPath, ["-p", fullPrompt, "--output-format", "text"], { - env: { - ...process.env, - CURSOR_API_KEY: apiKey, - GITHUB_INSTALLATION_TOKEN: githubInstallationToken, - }, - stdio: ["pipe", "pipe", "pipe"], - }); + const child = spawn( + cliPath, + ["--print", fullPrompt, "--output-format", "text", "--approve-mcps"], + { + cwd: process.cwd(), // Run in current working directory + env: { + ...process.env, + CURSOR_API_KEY: apiKey, + GITHUB_INSTALLATION_TOKEN: githubInstallationToken, + HOME: tempDir, // Set HOME so Cursor CLI can find .cursor/mcp.json + }, + stdio: ["ignore", "pipe", "pipe"], // Ignore stdin, pipe stdout/stderr + } + ); let stdout = ""; let stderr = ""; + let hasOutput = false; + + // Set a timeout to detect if the process hangs + const timeout = setTimeout(() => { + if (!hasOutput && child.exitCode === null) { + log.warning("Cursor CLI appears to be hanging, killing process..."); + child.kill("SIGTERM"); + resolve({ + success: false, + error: "Cursor CLI timed out - no output received", + output: stdout.trim(), + }); + } + }, 300000); // 5 minute timeout + + // Log when process starts + child.on("spawn", () => { + log.debug("Cursor CLI process spawned"); + }); child.stdout?.on("data", (data) => { + hasOutput = true; const text = data.toString(); stdout += text; // Stream output in real-time @@ -137,13 +84,22 @@ export const cursor = agent({ }); child.stderr?.on("data", (data) => { + hasOutput = true; const text = data.toString(); stderr += text; - // Log errors as they come + // Log errors as they come - but also write to stdout so we can see it + process.stderr.write(text); log.warning(text); }); - child.on("close", (code) => { + // Handle process exit + child.on("close", (code, signal) => { + clearTimeout(timeout); + + if (signal) { + log.warning(`Cursor CLI terminated by signal: ${signal}`); + } + if (code === 0) { log.success("Cursor CLI completed successfully"); resolve({ @@ -182,3 +138,54 @@ export const cursor = agent({ } }, }); + +function configureMcpServers({ + mcpServers, + cliPath, +}: { + mcpServers: Record; + cliPath: string; +}): void { + log.info("Configuring MCP servers for Cursor..."); + + // Cursor CLI reads MCP servers from .cursor/mcp.json or ~/.cursor/mcp.json + // Since we set HOME=tempDir during installation, we need to create the config file there + // Find the temp directory from the cliPath (it's in tempDir/.local/bin/cursor-agent) + const tempDir = cliPath.split("/.local/bin/")[0]; + const cursorConfigDir = join(tempDir, ".cursor"); + const mcpConfigPath = join(cursorConfigDir, "mcp.json"); + + // Build MCP configuration object + const mcpConfig: { mcpServers: Record } = { + mcpServers: {}, + }; + + for (const [serverName, serverConfig] of Object.entries(mcpServers)) { + // Only configure stdio servers (Cursor CLI supports stdio MCP servers) + // Check if it's a stdio server config (has 'command' property) + if (!("command" in serverConfig)) { + log.warning(`MCP server '${serverName}' is not a stdio server, skipping...`); + continue; + } + + // Add the server configuration + mcpConfig.mcpServers[serverName] = serverConfig; + log.info(`Adding MCP server '${serverName}'...`); + } + + if (Object.keys(mcpConfig.mcpServers).length === 0) { + log.info("No MCP servers to configure"); + return; + } + + // Create .cursor directory if it doesn't exist + mkdirSync(cursorConfigDir, { recursive: true }); + + // Write MCP configuration file + writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), "utf-8"); + log.info(`✓ MCP configuration written to ${mcpConfigPath}`); + + // Cursor CLI may require approval for MCP servers + // Use --approve-mcps flag when running to automatically approve all MCP servers + log.info("MCP servers configured. Cursor CLI will use --approve-mcps to auto-approve servers."); +} diff --git a/agents/shared.ts b/agents/shared.ts index 82739b4..fc5c98a 100644 --- a/agents/shared.ts +++ b/agents/shared.ts @@ -127,6 +127,80 @@ export async function installFromNpmTarball({ return cliPath; } +/** + * Install a CLI tool from a curl-based install script + * Downloads the install script, runs it with HOME set to temp directory, and returns the path to the CLI executable + * The temp directory will be cleaned up by the OS automatically + */ +export async function installFromCurl({ + installUrl, + executableName, +}: { + installUrl: string; + executableName: string; +}): Promise { + log.info(`📦 Installing ${executableName}...`); + + // Derive temp directory prefix from executable name (sanitize similar to package name) + // Replace any special characters with - and ensure trailing - + const tempDirPrefix = executableName.replace(/[^a-zA-Z0-9]/g, "-") + "-"; + + // Create temp directory + const tempDir = await mkdtemp(join(tmpdir(), tempDirPrefix)); + const installScriptPath = join(tempDir, "install.sh"); + + // Download the install script + log.info(`Downloading install script from ${installUrl}...`); + const installScriptResponse = await fetch(installUrl); + if (!installScriptResponse.ok) { + throw new Error(`Failed to download install script: ${installScriptResponse.status}`); + } + + if (!installScriptResponse.body) throw new Error("Response body is null"); + const fileStream = createWriteStream(installScriptPath); + await pipeline(installScriptResponse.body, fileStream); + log.info(`Downloaded install script to ${installScriptPath}`); + + // Make install script executable + chmodSync(installScriptPath, 0o755); + + log.info("Installing to temp directory..."); + + // Run the install script with HOME set to temp directory + // The Cursor install script installs to $HOME/.local/bin/{executableName} + // By setting HOME=tempDir, we ensure it installs to tempDir/.local/bin/{executableName} + const installResult = spawnSync("bash", [installScriptPath], { + cwd: tempDir, + env: { + ...process.env, + HOME: tempDir, // Cursor install script uses HOME for installation path + }, + stdio: "pipe", + encoding: "utf-8", + }); + + if (installResult.status !== 0) { + const errorOutput = installResult.stderr || installResult.stdout || "No output"; + throw new Error( + `Failed to install ${executableName}. Install script exited with code ${installResult.status}. Output: ${errorOutput}` + ); + } + + // The Cursor install script creates a symlink at $HOME/.local/bin/{executableName} + // Since we set HOME=tempDir, the deterministic path is: + const cliPath = join(tempDir, ".local", "bin", executableName); + + if (!existsSync(cliPath)) { + throw new Error(`Executable not found at ${cliPath}`); + } + + // Ensure binary is executable + chmodSync(cliPath, 0o755); + log.info(`✓ ${executableName} installed at ${cliPath}`); + + return cliPath; +} + export const agent = (agent: agent): agent => { return agent; }; diff --git a/fixtures/basic.txt b/fixtures/basic.txt index bb7a670..ae92a71 100644 --- a/fixtures/basic.txt +++ b/fixtures/basic.txt @@ -1 +1 @@ -create a comment on https://github.com/pullfrogai/scratch/issues/21 with an implementation of an mcp tool for fetching issue comments from github +list out the available mcp servers and what they do \ No newline at end of file diff --git a/play.ts b/play.ts index a9d1d37..351d246 100644 --- a/play.ts +++ b/play.ts @@ -24,7 +24,7 @@ export async function run( const inputs: Required = { prompt, - agent: "codex", + agent: "cursor", ...flatMorph(agents, (_, agent) => [ agent.inputKey, process.env[agent.inputKey.toUpperCase()], From 49cb15912456b46eecc3a081c800f15ecbaf3a9d Mon Sep 17 00:00:00 2001 From: Shawn Morreau Date: Fri, 14 Nov 2025 16:13:50 -0500 Subject: [PATCH 03/10] continue --- agents/cursor.ts | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/agents/cursor.ts b/agents/cursor.ts index 1f1808a..8360680 100644 --- a/agents/cursor.ts +++ b/agents/cursor.ts @@ -93,20 +93,10 @@ export const cursor = agent({ }); // Handle process exit - child.on("close", (code, signal) => { + child.on("close", (code) => { clearTimeout(timeout); - if (signal) { - log.warning(`Cursor CLI terminated by signal: ${signal}`); - } - - if (code === 0) { - log.success("Cursor CLI completed successfully"); - resolve({ - success: true, - output: stdout.trim(), - }); - } else { + if (code !== 0) { const errorMessage = stderr || `Cursor CLI exited with code ${code}`; log.error(`Cursor CLI failed: ${errorMessage}`); resolve({ @@ -148,9 +138,6 @@ function configureMcpServers({ }): void { log.info("Configuring MCP servers for Cursor..."); - // Cursor CLI reads MCP servers from .cursor/mcp.json or ~/.cursor/mcp.json - // Since we set HOME=tempDir during installation, we need to create the config file there - // Find the temp directory from the cliPath (it's in tempDir/.local/bin/cursor-agent) const tempDir = cliPath.split("/.local/bin/")[0]; const cursorConfigDir = join(tempDir, ".cursor"); const mcpConfigPath = join(cursorConfigDir, "mcp.json"); From e218afc35c4cd00cd3a171a648137fda09346ce2 Mon Sep 17 00:00:00 2001 From: Shawn Morreau Date: Tue, 18 Nov 2025 11:26:41 -0500 Subject: [PATCH 04/10] continue --- agents/cursor.ts | 16 +++++++++++++--- play.ts | 5 +++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/agents/cursor.ts b/agents/cursor.ts index 8360680..78ed6ff 100644 --- a/agents/cursor.ts +++ b/agents/cursor.ts @@ -8,7 +8,7 @@ import { agent, installFromCurl } from "./shared.ts"; export const cursor = agent({ name: "cursor", - inputKey: "cursor_api_key", + inputKeys: ["cursor_api_key"], install: async () => { return await installFromCurl({ installUrl: "https://cursor.com/install", @@ -93,10 +93,20 @@ export const cursor = agent({ }); // Handle process exit - child.on("close", (code) => { + child.on("close", (code, signal) => { clearTimeout(timeout); - if (code !== 0) { + if (signal) { + log.warning(`Cursor CLI terminated by signal: ${signal}`); + } + + if (code === 0) { + log.success("Cursor CLI completed successfully"); + resolve({ + success: true, + output: stdout.trim(), + }); + } else { const errorMessage = stderr || `Cursor CLI exited with code ${code}`; log.error(`Cursor CLI failed: ${errorMessage}`); resolve({ diff --git a/play.ts b/play.ts index 67c5230..b1e803c 100644 --- a/play.ts +++ b/play.ts @@ -29,6 +29,11 @@ export async function run( agent.inputKeys.map((inputKey) => [inputKey, process.env[inputKey.toUpperCase()]]) ), }; + // agent: "cursor", + // ...flatMorph(agents, (_, agent) => [ + // agent.inputKey, + // process.env[agent.inputKey.toUpperCase()], + // ]), const result = await main(inputs); From fc1b035f5d66e82819fd11004871d3af99299682 Mon Sep 17 00:00:00 2001 From: Shawn Morreau Date: Tue, 18 Nov 2025 18:41:45 -0500 Subject: [PATCH 05/10] fix pnpm play for cursor with MCP access --- agents/cursor.ts | 160 +++++++++++++++++++++++---------------------- fixtures/basic.txt | 2 +- mcp/server.ts | 5 +- mcp/shared.ts | 87 +++++++++++++++++++++++- 4 files changed, 174 insertions(+), 80 deletions(-) diff --git a/agents/cursor.ts b/agents/cursor.ts index 78ed6ff..c9270ad 100644 --- a/agents/cursor.ts +++ b/agents/cursor.ts @@ -1,10 +1,9 @@ import { spawn } from "node:child_process"; -import { mkdirSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; -import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join, resolve } from "node:path"; import { log } from "../utils/cli.ts"; import { addInstructions } from "./instructions.ts"; -import { agent, installFromCurl } from "./shared.ts"; +import { agent, installFromCurl, type AddMcpServerParams } from "./shared.ts"; export const cursor = agent({ name: "cursor", @@ -15,15 +14,88 @@ export const cursor = agent({ executableName: "cursor-agent", }); }, - run: async ({ prompt, mcpServers, apiKey, cliPath, githubInstallationToken }) => { + addMcpServer: ({ serverName, serverConfig, cliPath }: AddMcpServerParams) => { + const command = serverConfig.command; + const args = serverConfig.args || []; + const envVars = serverConfig.env || {}; + + // Resolve command to absolute path if it's a relative path + // For commands like "node", keep as-is; for file paths, resolve them + let resolvedCommand = command; + if (!command.includes("/") && !command.includes("\\")) { + // It's a command in PATH (like "node"), keep as-is + resolvedCommand = command; + } else { + // It's a file path, resolve to absolute path + resolvedCommand = resolve(command); + } + + // Resolve args to absolute paths if they look like file paths + const resolvedArgs = args.map((arg) => { + // If arg looks like a file path and is relative, resolve it + if ( + (arg.includes("/") || arg.includes("\\")) && + !arg.startsWith("/") && + !arg.match(/^[A-Z]:\\/) + ) { + return resolve(arg); + } + return arg; + }); + + // Build the server config with resolved paths + const resolvedServerConfig = { + command: resolvedCommand, + args: resolvedArgs, + env: envVars, + }; + + const tempDir = cliPath.split("/.local/bin/")[0]; + const cursorConfigDir = join(tempDir, ".cursor"); + const mcpConfigPath = join(cursorConfigDir, "mcp.json"); + + // Read existing config if it exists + let mcpConfig: { mcpServers: Record } = { mcpServers: {} }; + if (existsSync(mcpConfigPath)) { + try { + const existingConfig = readFileSync(mcpConfigPath, "utf-8"); + mcpConfig = JSON.parse(existingConfig); + if (!mcpConfig.mcpServers || typeof mcpConfig.mcpServers !== "object") { + mcpConfig.mcpServers = {}; + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to read existing MCP config at ${mcpConfigPath}: ${errorMessage}`); + } + } + + // Add the new server + mcpConfig.mcpServers[serverName] = resolvedServerConfig; + log.info(`Adding MCP server '${serverName}' to Cursor configuration...`); + + // Create .cursor directory if it doesn't exist + try { + mkdirSync(cursorConfigDir, { recursive: true }); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + throw new Error( + `Failed to create Cursor config directory at ${cursorConfigDir}: ${errorMessage}` + ); + } + + // Write MCP configuration file + try { + writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), "utf-8"); + log.info(`✓ MCP server '${serverName}' added to ${mcpConfigPath}`); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to write MCP config to ${mcpConfigPath}: ${errorMessage}`); + } + }, + run: async ({ prompt, apiKey, cliPath, githubInstallationToken }) => { process.env.CURSOR_API_KEY = apiKey; process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken; - // Configure MCP servers for Cursor (global config is fine - not part of repo) - if (mcpServers && Object.keys(mcpServers).length > 0) { - configureMcpServers({ mcpServers, cliPath }); - } - try { // Run cursor-agent in non-interactive mode with the prompt // Using -p flag for prompt, --output-format text for plain text output @@ -40,7 +112,7 @@ export const cursor = agent({ return new Promise((resolve) => { const child = spawn( cliPath, - ["--print", fullPrompt, "--output-format", "text", "--approve-mcps"], + ["--print", fullPrompt, "--output-format", "text", "--approve-mcps", "--force"], { cwd: process.cwd(), // Run in current working directory env: { @@ -55,20 +127,6 @@ export const cursor = agent({ let stdout = ""; let stderr = ""; - let hasOutput = false; - - // Set a timeout to detect if the process hangs - const timeout = setTimeout(() => { - if (!hasOutput && child.exitCode === null) { - log.warning("Cursor CLI appears to be hanging, killing process..."); - child.kill("SIGTERM"); - resolve({ - success: false, - error: "Cursor CLI timed out - no output received", - output: stdout.trim(), - }); - } - }, 300000); // 5 minute timeout // Log when process starts child.on("spawn", () => { @@ -76,7 +134,6 @@ export const cursor = agent({ }); child.stdout?.on("data", (data) => { - hasOutput = true; const text = data.toString(); stdout += text; // Stream output in real-time @@ -84,7 +141,6 @@ export const cursor = agent({ }); child.stderr?.on("data", (data) => { - hasOutput = true; const text = data.toString(); stderr += text; // Log errors as they come - but also write to stdout so we can see it @@ -94,8 +150,6 @@ export const cursor = agent({ // Handle process exit child.on("close", (code, signal) => { - clearTimeout(timeout); - if (signal) { log.warning(`Cursor CLI terminated by signal: ${signal}`); } @@ -138,51 +192,3 @@ export const cursor = agent({ } }, }); - -function configureMcpServers({ - mcpServers, - cliPath, -}: { - mcpServers: Record; - cliPath: string; -}): void { - log.info("Configuring MCP servers for Cursor..."); - - const tempDir = cliPath.split("/.local/bin/")[0]; - const cursorConfigDir = join(tempDir, ".cursor"); - const mcpConfigPath = join(cursorConfigDir, "mcp.json"); - - // Build MCP configuration object - const mcpConfig: { mcpServers: Record } = { - mcpServers: {}, - }; - - for (const [serverName, serverConfig] of Object.entries(mcpServers)) { - // Only configure stdio servers (Cursor CLI supports stdio MCP servers) - // Check if it's a stdio server config (has 'command' property) - if (!("command" in serverConfig)) { - log.warning(`MCP server '${serverName}' is not a stdio server, skipping...`); - continue; - } - - // Add the server configuration - mcpConfig.mcpServers[serverName] = serverConfig; - log.info(`Adding MCP server '${serverName}'...`); - } - - if (Object.keys(mcpConfig.mcpServers).length === 0) { - log.info("No MCP servers to configure"); - return; - } - - // Create .cursor directory if it doesn't exist - mkdirSync(cursorConfigDir, { recursive: true }); - - // Write MCP configuration file - writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), "utf-8"); - log.info(`✓ MCP configuration written to ${mcpConfigPath}`); - - // Cursor CLI may require approval for MCP servers - // Use --approve-mcps flag when running to automatically approve all MCP servers - log.info("MCP servers configured. Cursor CLI will use --approve-mcps to auto-approve servers."); -} diff --git a/fixtures/basic.txt b/fixtures/basic.txt index ae92a71..1e228fb 100644 --- a/fixtures/basic.txt +++ b/fixtures/basic.txt @@ -1 +1 @@ -list out the available mcp servers and what they do \ No newline at end of file +create a comment on https://github.com/pullfrogai/scratch/pull/29 that says ribbit ribbit \ No newline at end of file diff --git a/mcp/server.ts b/mcp/server.ts index 847ae8b..147442b 100644 --- a/mcp/server.ts +++ b/mcp/server.ts @@ -11,7 +11,10 @@ import { IssueTool } from "./issue.ts"; import { PullRequestTool } from "./pr.ts"; import { PullRequestInfoTool } from "./prInfo.ts"; import { ReviewTool } from "./review.ts"; -import { addTools } from "./shared.ts"; +import { addTools, initLogFile } from "./shared.ts"; + +// Initialize log file when server starts +initLogFile(); const server = new FastMCP({ name: "gh-pullfrog", diff --git a/mcp/shared.ts b/mcp/shared.ts index 522ee3c..d433f40 100644 --- a/mcp/shared.ts +++ b/mcp/shared.ts @@ -1,4 +1,6 @@ import { cached } from "@ark/util"; +import { appendFileSync } from "node:fs"; +import { join } from "node:path"; import { Octokit } from "@octokit/rest"; import type { StandardSchemaV1 } from "@standard-schema/spec"; import type { FastMCP, Tool } from "fastmcp"; @@ -30,7 +32,90 @@ export interface McpContext extends RepoContext { octokit: Octokit; } -export const tool = (tool: Tool>) => tool; +/** + * Get the log file path + */ +function getLogPath(): string { + return join(process.cwd(), "log.txt"); +} + +/** + * Initialize the log file with server startup information + */ +export function initLogFile(): void { + try { + const logPath = getLogPath(); + const timestamp = new Date().toISOString(); + const logEntry = `[${timestamp}] MCP Server Started: gh-pullfrog\n`; + appendFileSync(logPath, logEntry, "utf-8"); + } catch { + // Silently fail if logging fails to avoid breaking the tool + } +} + +/** + * Log MCP tool call information to log.txt + */ +function logToolCall({ + toolName, + request, + error, + success, +}: { + toolName: string; + request: unknown; + error?: unknown; + success?: boolean; +}): void { + try { + const logPath = getLogPath(); + const timestamp = new Date().toISOString(); + const requestStr = JSON.stringify(request, null, 2); + + let logEntry = `[${timestamp}] Tool: ${toolName}\n`; + logEntry += `Request: ${requestStr}\n`; + + if (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + const errorStack = error instanceof Error ? error.stack : undefined; + logEntry += `Error: ${errorMessage}\n`; + if (errorStack) { + logEntry += `Stack: ${errorStack}\n`; + } + logEntry += `Status: FAILED\n`; + } else if (success !== undefined) { + logEntry += `Status: ${success ? "SUCCESS" : "FAILED"}\n`; + } + + logEntry += `${"=".repeat(80)}\n\n`; + appendFileSync(logPath, logEntry, "utf-8"); + } catch { + // Silently fail if logging fails to avoid breaking the tool + } +} + +export const tool = (toolDef: Tool>) => { + // 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 { + logToolCall({ toolName, request: args }); + const result = await originalExecute(args, context); + // Check if result is a ToolResult with isError property + const isError = + result && typeof result === "object" && "isError" in result && result.isError === true; + logToolCall({ toolName, request: args, success: !isError }); + return result; + } catch (error) { + logToolCall({ toolName, request: args, error }); + throw error; + } + }; + + return toolDef; +}; export const addTools = (server: FastMCP, tools: Tool[]) => { for (const tool of tools) { From c72d44382f96595f87dc716ca8e23fbee4dbcae7 Mon Sep 17 00:00:00 2001 From: Shawn Morreau Date: Tue, 18 Nov 2025 19:02:47 -0500 Subject: [PATCH 06/10] logging --- mcp/server.ts | 5 +---- mcp/shared.ts | 16 +--------------- play.ts | 5 ----- 3 files changed, 2 insertions(+), 24 deletions(-) diff --git a/mcp/server.ts b/mcp/server.ts index 147442b..847ae8b 100644 --- a/mcp/server.ts +++ b/mcp/server.ts @@ -11,10 +11,7 @@ import { IssueTool } from "./issue.ts"; import { PullRequestTool } from "./pr.ts"; import { PullRequestInfoTool } from "./prInfo.ts"; import { ReviewTool } from "./review.ts"; -import { addTools, initLogFile } from "./shared.ts"; - -// Initialize log file when server starts -initLogFile(); +import { addTools } from "./shared.ts"; const server = new FastMCP({ name: "gh-pullfrog", diff --git a/mcp/shared.ts b/mcp/shared.ts index d433f40..21be66b 100644 --- a/mcp/shared.ts +++ b/mcp/shared.ts @@ -1,6 +1,6 @@ -import { cached } from "@ark/util"; import { appendFileSync } from "node:fs"; import { join } from "node:path"; +import { cached } from "@ark/util"; import { Octokit } from "@octokit/rest"; import type { StandardSchemaV1 } from "@standard-schema/spec"; import type { FastMCP, Tool } from "fastmcp"; @@ -39,20 +39,6 @@ function getLogPath(): string { return join(process.cwd(), "log.txt"); } -/** - * Initialize the log file with server startup information - */ -export function initLogFile(): void { - try { - const logPath = getLogPath(); - const timestamp = new Date().toISOString(); - const logEntry = `[${timestamp}] MCP Server Started: gh-pullfrog\n`; - appendFileSync(logPath, logEntry, "utf-8"); - } catch { - // Silently fail if logging fails to avoid breaking the tool - } -} - /** * Log MCP tool call information to log.txt */ diff --git a/play.ts b/play.ts index b1e803c..67c5230 100644 --- a/play.ts +++ b/play.ts @@ -29,11 +29,6 @@ export async function run( agent.inputKeys.map((inputKey) => [inputKey, process.env[inputKey.toUpperCase()]]) ), }; - // agent: "cursor", - // ...flatMorph(agents, (_, agent) => [ - // agent.inputKey, - // process.env[agent.inputKey.toUpperCase()], - // ]), const result = await main(inputs); From 3982b147f91019f90a0ec3bdfc5d95b2c849e0c3 Mon Sep 17 00:00:00 2001 From: Shawn Morreau Date: Tue, 18 Nov 2025 19:20:26 -0500 Subject: [PATCH 07/10] log --- agents/codex.ts | 1 - agents/cursor.ts | 2 +- mcp/shared.ts | 39 +++++++++++++++++---------------------- 3 files changed, 18 insertions(+), 24 deletions(-) diff --git a/agents/codex.ts b/agents/codex.ts index ce8d5f3..76c6234 100644 --- a/agents/codex.ts +++ b/agents/codex.ts @@ -1,5 +1,4 @@ import { spawnSync } from "node:child_process"; -import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk"; import { Codex, type CodexOptions, type ThreadEvent } from "@openai/codex-sdk"; import { log } from "../utils/cli.ts"; import { addInstructions } from "./instructions.ts"; diff --git a/agents/cursor.ts b/agents/cursor.ts index c9270ad..f2ed72c 100644 --- a/agents/cursor.ts +++ b/agents/cursor.ts @@ -3,7 +3,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { join, resolve } from "node:path"; import { log } from "../utils/cli.ts"; import { addInstructions } from "./instructions.ts"; -import { agent, installFromCurl, type AddMcpServerParams } from "./shared.ts"; +import { type AddMcpServerParams, agent, installFromCurl } from "./shared.ts"; export const cursor = agent({ name: "cursor", diff --git a/mcp/shared.ts b/mcp/shared.ts index 21be66b..3a46f64 100644 --- a/mcp/shared.ts +++ b/mcp/shared.ts @@ -52,32 +52,27 @@ function logToolCall({ request: unknown; error?: unknown; success?: boolean; -}): void { - try { - const logPath = getLogPath(); - const timestamp = new Date().toISOString(); - const requestStr = JSON.stringify(request, null, 2); +}) { + const logPath = getLogPath(); + const timestamp = new Date().toISOString(); + const requestStr = JSON.stringify(request, null, 2); - let logEntry = `[${timestamp}] Tool: ${toolName}\n`; - logEntry += `Request: ${requestStr}\n`; + let logEntry = `[${timestamp}] Tool: ${toolName}\nRequest: ${requestStr}\n`; - if (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - const errorStack = error instanceof Error ? error.stack : undefined; - logEntry += `Error: ${errorMessage}\n`; - if (errorStack) { - logEntry += `Stack: ${errorStack}\n`; - } - logEntry += `Status: FAILED\n`; - } else if (success !== undefined) { - logEntry += `Status: ${success ? "SUCCESS" : "FAILED"}\n`; + if (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + const errorStack = error instanceof Error ? error.stack : undefined; + logEntry += `Error: ${errorMessage}\n`; + if (errorStack) { + logEntry += `Stack: ${errorStack}\n`; } - - logEntry += `${"=".repeat(80)}\n\n`; - appendFileSync(logPath, logEntry, "utf-8"); - } catch { - // Silently fail if logging fails to avoid breaking the tool + logEntry += `Status: FAILED\n`; + } else if (success !== undefined) { + logEntry += `Status: ${success ? "SUCCESS" : "FAILED"}\n`; } + + logEntry += `${"=".repeat(80)}\n\n`; + appendFileSync(logPath, logEntry, "utf-8"); } export const tool = (toolDef: Tool>) => { From bf6212cae320537baed8a0040811d1182d0ad7e0 Mon Sep 17 00:00:00 2001 From: Shawn Morreau Date: Tue, 18 Nov 2025 20:02:25 -0500 Subject: [PATCH 08/10] undo david --- agents/claude.ts | 3 -- agents/codex.ts | 82 +++++++++++++++++++++++------------- agents/cursor.ts | 107 +++++++++++------------------------------------ agents/gemini.ts | 66 +++++++++++++++++------------ agents/shared.ts | 14 +------ main.ts | 14 +------ mcp/config.ts | 7 ++-- 7 files changed, 124 insertions(+), 169 deletions(-) diff --git a/agents/claude.ts b/agents/claude.ts index 91274ef..a00a036 100644 --- a/agents/claude.ts +++ b/agents/claude.ts @@ -15,9 +15,6 @@ export const claude = agent({ executablePath: "cli.js", }); }, - addMcpServer: () => { - // Claude accepts mcpServers directly in the query options, no configuration needed - }, run: async ({ prompt, mcpServers, apiKey, cliPath }) => { process.env.ANTHROPIC_API_KEY = apiKey; diff --git a/agents/codex.ts b/agents/codex.ts index 76c6234..88b99ec 100644 --- a/agents/codex.ts +++ b/agents/codex.ts @@ -1,4 +1,5 @@ import { spawnSync } from "node:child_process"; +import type { McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk"; import { Codex, type CodexOptions, type ThreadEvent } from "@openai/codex-sdk"; import { log } from "../utils/cli.ts"; import { addInstructions } from "./instructions.ts"; @@ -14,35 +15,20 @@ export const codex = agent({ executablePath: "bin/codex.js", }); }, - addMcpServer: ({ serverName, serverConfig, cliPath }) => { - // Codex CLI syntax: codex mcp add --env KEY=value -- [args...] - const command = serverConfig.command; - const args = serverConfig.args || []; - const envVars = serverConfig.env || {}; - - const addArgs = ["mcp", "add", serverName]; - - // 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) { - throw new Error( - `codex mcp add failed: ${addResult.stderr || addResult.stdout || "Unknown error"}` - ); - } - log.info(`✓ MCP server '${serverName}' configured`); - }, run: async ({ prompt, mcpServers, apiKey, cliPath, githubInstallationToken }) => { + // Configure MCP servers if provided + if (mcpServers && Object.keys(mcpServers).length > 0) { + // Filter to only stdio servers + const stdioServers: Record = {}; + for (const [serverName, serverConfig] of Object.entries(mcpServers)) { + if ("command" in serverConfig) { + stdioServers[serverName] = serverConfig; + } + } + if (Object.keys(stdioServers).length > 0) { + configureCodexMcpServers({ mcpServers: stdioServers, cliPath }); + } + } process.env.OPENAI_API_KEY = apiKey; process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken; @@ -182,3 +168,43 @@ const messageHandlers: { log.error(`Error: ${event.message}`); }, }; + +/** + * Configure MCP servers for Codex using the CLI. + * Codex CLI syntax: codex mcp add --env KEY=value -- [args...] + */ +function configureCodexMcpServers({ + mcpServers, + cliPath, +}: { + mcpServers: Record; + cliPath: string; +}): void { + for (const [serverName, serverConfig] of Object.entries(mcpServers)) { + const command = serverConfig.command; + const args = serverConfig.args || []; + const envVars = serverConfig.env || {}; + + const addArgs = ["mcp", "add", serverName]; + + // 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) { + throw new Error( + `codex mcp add failed: ${addResult.stderr || addResult.stdout || "Unknown error"}` + ); + } + log.info(`✓ MCP server '${serverName}' configured`); + } +} diff --git a/agents/cursor.ts b/agents/cursor.ts index f2ed72c..ad027f1 100644 --- a/agents/cursor.ts +++ b/agents/cursor.ts @@ -1,9 +1,10 @@ import { spawn } from "node:child_process"; -import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; -import { join, resolve } from "node:path"; +import { mkdirSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import type { McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk"; import { log } from "../utils/cli.ts"; import { addInstructions } from "./instructions.ts"; -import { type AddMcpServerParams, agent, installFromCurl } from "./shared.ts"; +import { agent, installFromCurl } from "./shared.ts"; export const cursor = agent({ name: "cursor", @@ -14,88 +15,12 @@ export const cursor = agent({ executableName: "cursor-agent", }); }, - addMcpServer: ({ serverName, serverConfig, cliPath }: AddMcpServerParams) => { - const command = serverConfig.command; - const args = serverConfig.args || []; - const envVars = serverConfig.env || {}; - - // Resolve command to absolute path if it's a relative path - // For commands like "node", keep as-is; for file paths, resolve them - let resolvedCommand = command; - if (!command.includes("/") && !command.includes("\\")) { - // It's a command in PATH (like "node"), keep as-is - resolvedCommand = command; - } else { - // It's a file path, resolve to absolute path - resolvedCommand = resolve(command); - } - - // Resolve args to absolute paths if they look like file paths - const resolvedArgs = args.map((arg) => { - // If arg looks like a file path and is relative, resolve it - if ( - (arg.includes("/") || arg.includes("\\")) && - !arg.startsWith("/") && - !arg.match(/^[A-Z]:\\/) - ) { - return resolve(arg); - } - return arg; - }); - - // Build the server config with resolved paths - const resolvedServerConfig = { - command: resolvedCommand, - args: resolvedArgs, - env: envVars, - }; - - const tempDir = cliPath.split("/.local/bin/")[0]; - const cursorConfigDir = join(tempDir, ".cursor"); - const mcpConfigPath = join(cursorConfigDir, "mcp.json"); - - // Read existing config if it exists - let mcpConfig: { mcpServers: Record } = { mcpServers: {} }; - if (existsSync(mcpConfigPath)) { - try { - const existingConfig = readFileSync(mcpConfigPath, "utf-8"); - mcpConfig = JSON.parse(existingConfig); - if (!mcpConfig.mcpServers || typeof mcpConfig.mcpServers !== "object") { - mcpConfig.mcpServers = {}; - } - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - throw new Error(`Failed to read existing MCP config at ${mcpConfigPath}: ${errorMessage}`); - } - } - - // Add the new server - mcpConfig.mcpServers[serverName] = resolvedServerConfig; - log.info(`Adding MCP server '${serverName}' to Cursor configuration...`); - - // Create .cursor directory if it doesn't exist - try { - mkdirSync(cursorConfigDir, { recursive: true }); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - throw new Error( - `Failed to create Cursor config directory at ${cursorConfigDir}: ${errorMessage}` - ); - } - - // Write MCP configuration file - try { - writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), "utf-8"); - log.info(`✓ MCP server '${serverName}' added to ${mcpConfigPath}`); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - throw new Error(`Failed to write MCP config to ${mcpConfigPath}: ${errorMessage}`); - } - }, - run: async ({ prompt, apiKey, cliPath, githubInstallationToken }) => { + run: async ({ prompt, apiKey, cliPath, githubInstallationToken, mcpServers }) => { process.env.CURSOR_API_KEY = apiKey; process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken; + configureCursorMcpServers({ mcpServers, cliPath }); + try { // Run cursor-agent in non-interactive mode with the prompt // Using -p flag for prompt, --output-format text for plain text output @@ -192,3 +117,21 @@ export const cursor = agent({ } }, }); + +/** + * Configure MCP servers for Cursor by writing to the Cursor configuration file. + * For cursor, we need to add the MCP servers to the Cursor configuration file manually as there is no CLI command to do this. + */ +function configureCursorMcpServers({ + mcpServers, + cliPath, +}: { + mcpServers: Record; + cliPath: string; +}) { + const tempDir = cliPath.split("/.local/bin/")[0]; + const cursorConfigDir = join(tempDir, ".cursor"); + const mcpConfigPath = join(cursorConfigDir, "mcp.json"); + mkdirSync(cursorConfigDir, { recursive: true }); + writeFileSync(mcpConfigPath, JSON.stringify({ mcpServers }, null, 2), "utf-8"); +} diff --git a/agents/gemini.ts b/agents/gemini.ts index 621ac2c..ba4ea1c 100644 --- a/agents/gemini.ts +++ b/agents/gemini.ts @@ -1,5 +1,5 @@ import { spawnSync } from "node:child_process"; -import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk"; +import type { McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk"; import { log } from "../utils/cli.ts"; import { spawn } from "../utils/subprocess.ts"; import { addInstructions } from "./instructions.ts"; @@ -16,33 +16,8 @@ export const gemini = agent({ installDependencies: true, }); }, - addMcpServer: ({ serverName, serverConfig, cliPath }) => { - // Gemini CLI syntax: gemini mcp add [args...] --env KEY=value - const command = serverConfig.command; - const args = serverConfig.args || []; - const envVars = serverConfig.env || {}; - - const addArgs = ["mcp", "add", serverName, command, ...args]; - - // Add environment variables as --env flags - 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) { - throw new Error( - `gemini mcp add failed: ${addResult.stderr || addResult.stdout || "Unknown error"}` - ); - } - log.info(`✓ MCP server '${serverName}' configured`); - }, run: async ({ prompt, apiKey, mcpServers, githubInstallationToken, cliPath }) => { + configureGeminiMcpServers({ mcpServers, cliPath }); if (!apiKey) { throw new Error("google_api_key or gemini_api_key is required for gemini agent"); } @@ -108,3 +83,40 @@ export const gemini = agent({ }, }); +/** + * Configure MCP servers for Gemini using the CLI. + * Gemini CLI syntax: gemini mcp add [args...] --env KEY=value + */ +function configureGeminiMcpServers({ + mcpServers, + cliPath, +}: { + mcpServers: Record; + cliPath: string; +}): void { + for (const [serverName, serverConfig] of Object.entries(mcpServers)) { + const command = serverConfig.command; + const args = serverConfig.args || []; + const envVars = serverConfig.env || {}; + + const addArgs = ["mcp", "add", serverName, command, ...args]; + + // Add environment variables as --env flags + 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) { + throw new Error( + `gemini mcp add failed: ${addResult.stderr || addResult.stdout || "Unknown error"}` + ); + } + log.info(`✓ MCP server '${serverName}' configured`); + } +} diff --git a/agents/shared.ts b/agents/shared.ts index ffc0a6b..ac7c81e 100644 --- a/agents/shared.ts +++ b/agents/shared.ts @@ -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 { McpServerConfig } from "@anthropic-ai/claude-agent-sdk"; +import type { McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk"; import { log } from "../utils/cli.ts"; /** @@ -24,7 +24,7 @@ export interface AgentConfig { apiKey: string; githubInstallationToken: string; prompt: string; - mcpServers: Record; + mcpServers: Record; cliPath: string; } @@ -223,19 +223,9 @@ export const agent = (agent: agent): agent => { return agent; }; -/** - * Parameters for adding an MCP server to an agent - */ -export interface AddMcpServerParams { - serverName: string; - serverConfig: Extract; - cliPath: string; -} - export type Agent = { name: string; inputKeys: string[]; install: () => Promise; - addMcpServer: (params: AddMcpServerParams) => void; run: (config: AgentConfig) => Promise; }; diff --git a/main.ts b/main.ts index ba761a6..dd09ff4 100644 --- a/main.ts +++ b/main.ts @@ -1,7 +1,7 @@ import { flatMorph } from "@ark/util"; import { type } from "arktype"; import { agents } from "./agents/index.ts"; -import { createMcpConfigs, forEachStdioMcpServer } from "./mcp/config.ts"; +import { createMcpConfigs } from "./mcp/config.ts"; import packageJson from "./package.json" with { type: "json" }; import { fetchRepoSettings } from "./utils/api.ts"; import { log } from "./utils/cli.ts"; @@ -141,18 +141,6 @@ export async function main(inputs: Inputs): Promise { const apiKey = inputs[matchingInputKey]!; - // Configure MCP servers if provided (global config is fine - not part of repo) - if (mcpServers && Object.keys(mcpServers).length > 0) { - log.info(`Configuring MCP servers for ${agentName}...`); - forEachStdioMcpServer(mcpServers, (serverName, serverConfig) => { - agent.addMcpServer({ - serverName, - serverConfig, - cliPath, - }); - }); - } - const result = await agent.run({ prompt: inputs.prompt, mcpServers, diff --git a/mcp/config.ts b/mcp/config.ts index fde1572..469a7a3 100644 --- a/mcp/config.ts +++ b/mcp/config.ts @@ -2,7 +2,7 @@ * Simple MCP configuration helper for adding our minimal GitHub comment server */ -import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk"; +import type { McpServerConfig, McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk"; import { fromHere } from "@ark/fs"; import { log } from "../utils/cli.ts"; import { parseRepoContext } from "../utils/github.ts"; @@ -38,7 +38,7 @@ export function createMcpConfigs(githubInstallationToken: string): McpConfigs { */ export function forEachStdioMcpServer( mcpServers: Record, - handler: (serverName: string, serverConfig: Extract) => void + handler: (serverName: string, serverConfig: McpStdioServerConfig) => void ): void { for (const [serverName, serverConfig] of Object.entries(mcpServers)) { // Only configure stdio servers (CLIs support stdio MCP servers) @@ -46,7 +46,6 @@ export function forEachStdioMcpServer( log.warning(`MCP server '${serverName}' is not a stdio server, skipping...`); continue; } - handler(serverName, serverConfig as Extract); + handler(serverName, serverConfig); } } - From 0ac4975b50914ae51c079b1c293f3ae88e5a16d5 Mon Sep 17 00:00:00 2001 From: Shawn Morreau Date: Tue, 18 Nov 2025 20:10:39 -0500 Subject: [PATCH 09/10] fix agents --- fixtures/basic.txt | 2 +- mcp/config.ts | 2 +- play.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fixtures/basic.txt b/fixtures/basic.txt index 1e228fb..e365fe2 100644 --- a/fixtures/basic.txt +++ b/fixtures/basic.txt @@ -1 +1 @@ -create a comment on https://github.com/pullfrogai/scratch/pull/29 that says ribbit ribbit \ No newline at end of file +create a comment on https://github.com/pullfrogai/scratch/pull/29 that says GEM ribbit \ No newline at end of file diff --git a/mcp/config.ts b/mcp/config.ts index 469a7a3..d1abed0 100644 --- a/mcp/config.ts +++ b/mcp/config.ts @@ -10,7 +10,7 @@ import { ghPullfrogMcpName } from "./index.ts"; export type McpName = typeof ghPullfrogMcpName; -export type McpConfigs = Record; +export type McpConfigs = Record; export function createMcpConfigs(githubInstallationToken: string): McpConfigs { const repoContext = parseRepoContext(); diff --git a/play.ts b/play.ts index 67c5230..5e972a6 100644 --- a/play.ts +++ b/play.ts @@ -24,7 +24,7 @@ export async function run( const inputs: Required = { prompt, - agent: "cursor", + agent: "gemini", ...flatMorph(agents, (_, agent) => agent.inputKeys.map((inputKey) => [inputKey, process.env[inputKey.toUpperCase()]]) ), From 7bbca2fdebef3fa2b81dd22e659236b9d92648c6 Mon Sep 17 00:00:00 2001 From: Shawn Morreau Date: Tue, 18 Nov 2025 20:18:00 -0500 Subject: [PATCH 10/10] remove slop --- agents/codex.ts | 26 ++++-------------------- agents/cursor.ts | 11 ++--------- agents/gemini.ts | 11 ++--------- agents/shared.ts | 51 +++++++++++++++++++++++++++++++++++------------- 4 files changed, 45 insertions(+), 54 deletions(-) diff --git a/agents/codex.ts b/agents/codex.ts index 88b99ec..52de896 100644 --- a/agents/codex.ts +++ b/agents/codex.ts @@ -1,9 +1,8 @@ import { spawnSync } from "node:child_process"; -import type { McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk"; import { Codex, type CodexOptions, type ThreadEvent } from "@openai/codex-sdk"; import { log } from "../utils/cli.ts"; import { addInstructions } from "./instructions.ts"; -import { agent, installFromNpmTarball } from "./shared.ts"; +import { agent, type ConfigureMcpServersParams, installFromNpmTarball } from "./shared.ts"; export const codex = agent({ name: "codex", @@ -16,22 +15,11 @@ export const codex = agent({ }); }, run: async ({ prompt, mcpServers, apiKey, cliPath, githubInstallationToken }) => { - // Configure MCP servers if provided - if (mcpServers && Object.keys(mcpServers).length > 0) { - // Filter to only stdio servers - const stdioServers: Record = {}; - for (const [serverName, serverConfig] of Object.entries(mcpServers)) { - if ("command" in serverConfig) { - stdioServers[serverName] = serverConfig; - } - } - if (Object.keys(stdioServers).length > 0) { - configureCodexMcpServers({ mcpServers: stdioServers, cliPath }); - } - } process.env.OPENAI_API_KEY = apiKey; process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken; + configureCodexMcpServers({ mcpServers, cliPath }); + // Configure Codex const codexOptions: CodexOptions = { apiKey, @@ -173,13 +161,7 @@ const messageHandlers: { * Configure MCP servers for Codex using the CLI. * Codex CLI syntax: codex mcp add --env KEY=value -- [args...] */ -function configureCodexMcpServers({ - mcpServers, - cliPath, -}: { - mcpServers: Record; - cliPath: string; -}): void { +function configureCodexMcpServers({ mcpServers, cliPath }: ConfigureMcpServersParams): void { for (const [serverName, serverConfig] of Object.entries(mcpServers)) { const command = serverConfig.command; const args = serverConfig.args || []; diff --git a/agents/cursor.ts b/agents/cursor.ts index ad027f1..aa6508d 100644 --- a/agents/cursor.ts +++ b/agents/cursor.ts @@ -1,10 +1,9 @@ import { spawn } from "node:child_process"; import { mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; -import type { McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk"; import { log } from "../utils/cli.ts"; import { addInstructions } from "./instructions.ts"; -import { agent, installFromCurl } from "./shared.ts"; +import { agent, type ConfigureMcpServersParams, installFromCurl } from "./shared.ts"; export const cursor = agent({ name: "cursor", @@ -122,13 +121,7 @@ export const cursor = agent({ * Configure MCP servers for Cursor by writing to the Cursor configuration file. * For cursor, we need to add the MCP servers to the Cursor configuration file manually as there is no CLI command to do this. */ -function configureCursorMcpServers({ - mcpServers, - cliPath, -}: { - mcpServers: Record; - cliPath: string; -}) { +function configureCursorMcpServers({ mcpServers, cliPath }: ConfigureMcpServersParams) { const tempDir = cliPath.split("/.local/bin/")[0]; const cursorConfigDir = join(tempDir, ".cursor"); const mcpConfigPath = join(cursorConfigDir, "mcp.json"); diff --git a/agents/gemini.ts b/agents/gemini.ts index ba4ea1c..3890b61 100644 --- a/agents/gemini.ts +++ b/agents/gemini.ts @@ -1,9 +1,8 @@ import { spawnSync } from "node:child_process"; -import type { McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk"; import { log } from "../utils/cli.ts"; import { spawn } from "../utils/subprocess.ts"; import { addInstructions } from "./instructions.ts"; -import { agent, installFromNpmTarball } from "./shared.ts"; +import { agent, type ConfigureMcpServersParams, installFromNpmTarball } from "./shared.ts"; export const gemini = agent({ name: "gemini", @@ -87,13 +86,7 @@ export const gemini = agent({ * Configure MCP servers for Gemini using the CLI. * Gemini CLI syntax: gemini mcp add [args...] --env KEY=value */ -function configureGeminiMcpServers({ - mcpServers, - cliPath, -}: { - mcpServers: Record; - cliPath: string; -}): void { +function configureGeminiMcpServers({ mcpServers, cliPath }: ConfigureMcpServersParams): void { for (const [serverName, serverConfig] of Object.entries(mcpServers)) { const command = serverConfig.command; const args = serverConfig.args || []; diff --git a/agents/shared.ts b/agents/shared.ts index ac7c81e..0d43741 100644 --- a/agents/shared.ts +++ b/agents/shared.ts @@ -28,6 +28,40 @@ export interface AgentConfig { cliPath: string; } +/** + * Parameters for configuring MCP servers + */ +export interface ConfigureMcpServersParams { + mcpServers: Record; + cliPath: string; +} + +/** + * Parameters for installing from npm tarball + */ +export interface InstallFromNpmTarballParams { + packageName: string; + version: string; + executablePath: string; + installDependencies?: boolean; +} + +/** + * Parameters for installing from curl script + */ +export interface InstallFromCurlParams { + installUrl: string; + executableName: string; +} + +/** + * NPM registry response data structure + */ +export interface NpmRegistryData { + "dist-tags": { latest: string }; + versions: Record; +} + /** * Install a CLI tool from an npm package tarball * Downloads the tarball, extracts it to a temp directory, and returns the path to the CLI executable @@ -38,12 +72,7 @@ export async function installFromNpmTarball({ version, executablePath, installDependencies, -}: { - packageName: string; - version: string; - executablePath: string; - installDependencies?: boolean; -}): Promise { +}: InstallFromNpmTarballParams): Promise { // Resolve version if it's a range or "latest" let resolvedVersion = version; if (version.startsWith("^") || version.startsWith("~") || version === "latest") { @@ -54,10 +83,7 @@ export async function installFromNpmTarball({ if (!registryResponse.ok) { throw new Error(`Failed to query registry: ${registryResponse.status}`); } - const registryData = (await registryResponse.json()) as { - "dist-tags": { latest: string }; - versions: Record; - }; + const registryData = (await registryResponse.json()) as NpmRegistryData; resolvedVersion = registryData["dist-tags"].latest; log.info(`Resolved to version ${resolvedVersion}`); } catch (error) { @@ -153,10 +179,7 @@ export async function installFromNpmTarball({ export async function installFromCurl({ installUrl, executableName, -}: { - installUrl: string; - executableName: string; -}): Promise { +}: InstallFromCurlParams): Promise { log.info(`📦 Installing ${executableName}...`); // Derive temp directory prefix from executable name (sanitize similar to package name)