From fc1b035f5d66e82819fd11004871d3af99299682 Mon Sep 17 00:00:00 2001 From: Shawn Morreau Date: Tue, 18 Nov 2025 18:41:45 -0500 Subject: [PATCH] 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) {