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 ce8d5f3..52de896 100644 --- a/agents/codex.ts +++ b/agents/codex.ts @@ -1,9 +1,8 @@ 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"; -import { agent, installFromNpmTarball } from "./shared.ts"; +import { agent, type ConfigureMcpServersParams, installFromNpmTarball } from "./shared.ts"; export const codex = agent({ name: "codex", @@ -15,38 +14,12 @@ 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 }) => { process.env.OPENAI_API_KEY = apiKey; process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken; + configureCodexMcpServers({ mcpServers, cliPath }); + // Configure Codex const codexOptions: CodexOptions = { apiKey, @@ -183,3 +156,37 @@ 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 }: ConfigureMcpServersParams): 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 new file mode 100644 index 0000000..aa6508d --- /dev/null +++ b/agents/cursor.ts @@ -0,0 +1,130 @@ +import { spawn } from "node:child_process"; +import { mkdirSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { log } from "../utils/cli.ts"; +import { addInstructions } from "./instructions.ts"; +import { agent, type ConfigureMcpServersParams, installFromCurl } from "./shared.ts"; + +export const cursor = agent({ + name: "cursor", + inputKeys: ["cursor_api_key"], + install: async () => { + return await installFromCurl({ + installUrl: "https://cursor.com/install", + executableName: "cursor-agent", + }); + }, + 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 + // 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, + ["--print", fullPrompt, "--output-format", "text", "--approve-mcps", "--force"], + { + 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 = ""; + + // Log when process starts + child.on("spawn", () => { + log.debug("Cursor CLI process spawned"); + }); + + 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 - but also write to stdout so we can see it + process.stderr.write(text); + log.warning(text); + }); + + // Handle process exit + child.on("close", (code, signal) => { + 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({ + 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: "", + }; + } + }, +}); + +/** + * 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 }: ConfigureMcpServersParams) { + 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..3890b61 100644 --- a/agents/gemini.ts +++ b/agents/gemini.ts @@ -1,9 +1,8 @@ import { spawnSync } from "node:child_process"; -import type { McpServerConfig } 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", @@ -16,33 +15,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 +82,34 @@ 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 }: ConfigureMcpServersParams): 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/index.ts b/agents/index.ts index aa52a10..4af71c3 100644 --- a/agents/index.ts +++ b/agents/index.ts @@ -1,10 +1,12 @@ import { claude } from "./claude.ts"; import { codex } from "./codex.ts"; +import { cursor } from "./cursor.ts"; import { gemini } from "./gemini.ts"; export const agents = { claude, codex, + cursor, gemini, } as const; diff --git a/agents/shared.ts b/agents/shared.ts index 63a641e..0d43741 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,10 +24,44 @@ export interface AgentConfig { apiKey: string; githubInstallationToken: string; prompt: string; - mcpServers: Record; + mcpServers: Record; 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) { @@ -145,23 +171,84 @@ 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, +}: InstallFromCurlParams): 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; }; -/** - * 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/entry.js b/entry.js index 00e1543..4e86173 100755 --- a/entry.js +++ b/entry.js @@ -33346,6 +33346,49 @@ async function installFromNpmTarball({ log.info(`\u2713 ${packageName} installed at ${cliPath}`); return cliPath; } +async function installFromCurl({ + installUrl, + executableName +}) { + log.info(`\u{1F4E6} Installing ${executableName}...`); + const tempDirPrefix = executableName.replace(/[^a-zA-Z0-9]/g, "-") + "-"; + const tempDir = await mkdtemp(join4(tmpdir(), tempDirPrefix)); + const installScriptPath = join4(tempDir, "install.sh"); + 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 = createWriteStream2(installScriptPath); + await pipeline(installScriptResponse.body, fileStream); + log.info(`Downloaded install script to ${installScriptPath}`); + chmodSync(installScriptPath, 493); + log.info("Installing to temp directory..."); + 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}` + ); + } + const cliPath = join4(tempDir, ".local", "bin", executableName); + if (!existsSync2(cliPath)) { + throw new Error(`Executable not found at ${cliPath}`); + } + chmodSync(cliPath, 493); + log.info(`\u2713 ${executableName} installed at ${cliPath}`); + return cliPath; +} var agent = (agent2) => { return agent2; }; @@ -33962,6 +34005,139 @@ function configureMcpServers({ } } +// agents/cursor.ts +import { spawn as spawn3 } from "node:child_process"; +import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs"; +import { join as join5 } from "node:path"; +var cursor = agent({ + name: "cursor", + inputKey: "cursor_api_key", + 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; + if (mcpServers && Object.keys(mcpServers).length > 0) { + configureMcpServers2({ mcpServers, cliPath }); + } + try { + const fullPrompt = addInstructions(prompt); + const tempDir = cliPath.split("/.local/bin/")[0]; + log.info("Running Cursor CLI..."); + return new Promise((resolve) => { + const child = spawn3( + 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; + 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() + }); + } + }, 3e5); + child.on("spawn", () => { + log.debug("Cursor CLI process spawned"); + }); + child.stdout?.on("data", (data) => { + hasOutput = true; + const text = data.toString(); + stdout += text; + process.stdout.write(text); + }); + child.stderr?.on("data", (data) => { + hasOutput = true; + const text = data.toString(); + stderr += text; + process.stderr.write(text); + log.warning(text); + }); + child.on("close", (code) => { + clearTimeout(timeout); + if (code !== 0) { + 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", (error2) => { + const errorMessage = error2.message || String(error2); + log.error(`Cursor CLI execution failed: ${errorMessage}`); + resolve({ + success: false, + error: errorMessage, + output: stdout.trim() + }); + }); + }); + } catch (error2) { + const errorMessage = error2 instanceof Error ? error2.message : String(error2); + log.error(`Cursor execution failed: ${errorMessage}`); + return { + success: false, + error: errorMessage, + output: "" + }; + } + } +}); +function configureMcpServers2({ + mcpServers, + cliPath +}) { + log.info("Configuring MCP servers for Cursor..."); + const tempDir = cliPath.split("/.local/bin/")[0]; + const cursorConfigDir = join5(tempDir, ".cursor"); + const mcpConfigPath = join5(cursorConfigDir, "mcp.json"); + const mcpConfig = { + mcpServers: {} + }; + for (const [serverName, serverConfig] of Object.entries(mcpServers)) { + if (!("command" in serverConfig)) { + log.warning(`MCP server '${serverName}' is not a stdio server, skipping...`); + continue; + } + 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; + } + mkdirSync2(cursorConfigDir, { recursive: true }); + writeFileSync2(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), "utf-8"); + log.info(`\u2713 MCP configuration written to ${mcpConfigPath}`); + log.info("MCP servers configured. Cursor CLI will use --approve-mcps to auto-approve servers."); +} + // utils/github.ts var core2 = __toESM(require_core(), 1); import { createSign } from "node:crypto"; @@ -34152,7 +34328,7 @@ function parseRepoContext() { // utils/subprocess.ts import { spawn as nodeSpawn } from "node:child_process"; -async function spawn3(options) { +async function spawn4(options) { const { cmd, args: args2, env: env2, input, timeout, cwd: cwd4, onStdout, onStderr } = options; const startTime = Date.now(); let stdoutBuffer = ""; @@ -34254,7 +34430,7 @@ var jules = agent({ log.info(`Starting session with prompt: ${prompt.substring(0, 100)}...`); let sessionId; try { - const createResult = await spawn3({ + const createResult = await spawn4({ cmd: "node", args: [cliPath, "remote", "new", "--repo", repoName, "--session", sessionPrompt], onStdout: (chunk) => { @@ -34304,7 +34480,7 @@ var jules = agent({ await new Promise((resolve) => setTimeout(resolve, 1e4)); pollAttempts++; try { - const listResult = await spawn3({ + const listResult = await spawn4({ cmd: "node", args: [cliPath, "remote", "list", "--session"], onStdout: (chunk) => { @@ -34332,7 +34508,7 @@ var jules = agent({ if (sessionId) { try { log.info(`Pulling results for session ${sessionId}...`); - const pullResult = await spawn3({ + const pullResult = await spawn4({ cmd: "node", args: [cliPath, "remote", "pull", "--session", sessionId], onStdout: (chunk) => { @@ -34368,6 +34544,7 @@ var jules = agent({ var agents = { claude, codex, + cursor, jules }; @@ -42098,7 +42275,7 @@ var caller = (options = {}) => { }; // node_modules/.pnpm/@ark+fs@0.53.0/node_modules/@ark/fs/out/fs.js -import { dirname as dirname2, join as join5, parse } from "node:path"; +import { dirname as dirname2, join as join6, parse } from "node:path"; import * as process3 from "node:process"; import { URL as URL2, fileURLToPath as fileURLToPath4 } from "node:url"; var filePath = (path4) => { @@ -42112,7 +42289,7 @@ var filePath = (path4) => { return file; }; var dirOfCaller = () => dirname2(filePath(caller({ methodName: "dirOfCaller", upStackBy: 1 }).file)); -var fromHere = (...joinWith) => join5(dirOfCaller(), ...joinWith); +var fromHere = (...joinWith) => join6(dirOfCaller(), ...joinWith); var fsRoot = parse(process3.cwd()).root; // mcp/config.ts diff --git a/fixtures/basic.txt b/fixtures/basic.txt index bb7a670..e365fe2 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 +create a comment on https://github.com/pullfrogai/scratch/pull/29 that says GEM ribbit \ No newline at end of file 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..d1abed0 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"; @@ -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(); @@ -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); } } - diff --git a/mcp/shared.ts b/mcp/shared.ts index 522ee3c..3a46f64 100644 --- a/mcp/shared.ts +++ b/mcp/shared.ts @@ -1,3 +1,5 @@ +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"; @@ -30,7 +32,71 @@ 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"); +} + +/** + * Log MCP tool call information to log.txt + */ +function logToolCall({ + toolName, + request, + error, + success, +}: { + toolName: string; + request: unknown; + error?: unknown; + success?: boolean; +}) { + const logPath = getLogPath(); + const timestamp = new Date().toISOString(); + const requestStr = JSON.stringify(request, null, 2); + + 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`; + } + + logEntry += `${"=".repeat(80)}\n\n`; + appendFileSync(logPath, logEntry, "utf-8"); +} + +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) {