fix pnpm play for cursor with MCP access
This commit is contained in:
+83
-77
@@ -1,10 +1,9 @@
|
|||||||
import { spawn } from "node:child_process";
|
import { spawn } from "node:child_process";
|
||||||
import { mkdirSync, writeFileSync } from "node:fs";
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join, resolve } from "node:path";
|
||||||
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
|
||||||
import { log } from "../utils/cli.ts";
|
import { log } from "../utils/cli.ts";
|
||||||
import { addInstructions } from "./instructions.ts";
|
import { addInstructions } from "./instructions.ts";
|
||||||
import { agent, installFromCurl } from "./shared.ts";
|
import { agent, installFromCurl, type AddMcpServerParams } from "./shared.ts";
|
||||||
|
|
||||||
export const cursor = agent({
|
export const cursor = agent({
|
||||||
name: "cursor",
|
name: "cursor",
|
||||||
@@ -15,15 +14,88 @@ export const cursor = agent({
|
|||||||
executableName: "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<string, unknown> } = { 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.CURSOR_API_KEY = apiKey;
|
||||||
process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken;
|
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 {
|
try {
|
||||||
// Run cursor-agent in non-interactive mode with the prompt
|
// Run cursor-agent in non-interactive mode with the prompt
|
||||||
// Using -p flag for prompt, --output-format text for plain text output
|
// Using -p flag for prompt, --output-format text for plain text output
|
||||||
@@ -40,7 +112,7 @@ export const cursor = agent({
|
|||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const child = spawn(
|
const child = spawn(
|
||||||
cliPath,
|
cliPath,
|
||||||
["--print", fullPrompt, "--output-format", "text", "--approve-mcps"],
|
["--print", fullPrompt, "--output-format", "text", "--approve-mcps", "--force"],
|
||||||
{
|
{
|
||||||
cwd: process.cwd(), // Run in current working directory
|
cwd: process.cwd(), // Run in current working directory
|
||||||
env: {
|
env: {
|
||||||
@@ -55,20 +127,6 @@ export const cursor = agent({
|
|||||||
|
|
||||||
let stdout = "";
|
let stdout = "";
|
||||||
let stderr = "";
|
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
|
// Log when process starts
|
||||||
child.on("spawn", () => {
|
child.on("spawn", () => {
|
||||||
@@ -76,7 +134,6 @@ export const cursor = agent({
|
|||||||
});
|
});
|
||||||
|
|
||||||
child.stdout?.on("data", (data) => {
|
child.stdout?.on("data", (data) => {
|
||||||
hasOutput = true;
|
|
||||||
const text = data.toString();
|
const text = data.toString();
|
||||||
stdout += text;
|
stdout += text;
|
||||||
// Stream output in real-time
|
// Stream output in real-time
|
||||||
@@ -84,7 +141,6 @@ export const cursor = agent({
|
|||||||
});
|
});
|
||||||
|
|
||||||
child.stderr?.on("data", (data) => {
|
child.stderr?.on("data", (data) => {
|
||||||
hasOutput = true;
|
|
||||||
const text = data.toString();
|
const text = data.toString();
|
||||||
stderr += text;
|
stderr += text;
|
||||||
// Log errors as they come - but also write to stdout so we can see it
|
// 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
|
// Handle process exit
|
||||||
child.on("close", (code, signal) => {
|
child.on("close", (code, signal) => {
|
||||||
clearTimeout(timeout);
|
|
||||||
|
|
||||||
if (signal) {
|
if (signal) {
|
||||||
log.warning(`Cursor CLI terminated by signal: ${signal}`);
|
log.warning(`Cursor CLI terminated by signal: ${signal}`);
|
||||||
}
|
}
|
||||||
@@ -138,51 +192,3 @@ export const cursor = agent({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function configureMcpServers({
|
|
||||||
mcpServers,
|
|
||||||
cliPath,
|
|
||||||
}: {
|
|
||||||
mcpServers: Record<string, McpServerConfig>;
|
|
||||||
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<string, McpServerConfig> } = {
|
|
||||||
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.");
|
|
||||||
}
|
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
list out the available mcp servers and what they do
|
create a comment on https://github.com/pullfrogai/scratch/pull/29 that says ribbit ribbit
|
||||||
+4
-1
@@ -11,7 +11,10 @@ import { IssueTool } from "./issue.ts";
|
|||||||
import { PullRequestTool } from "./pr.ts";
|
import { PullRequestTool } from "./pr.ts";
|
||||||
import { PullRequestInfoTool } from "./prInfo.ts";
|
import { PullRequestInfoTool } from "./prInfo.ts";
|
||||||
import { ReviewTool } from "./review.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({
|
const server = new FastMCP({
|
||||||
name: "gh-pullfrog",
|
name: "gh-pullfrog",
|
||||||
|
|||||||
+86
-1
@@ -1,4 +1,6 @@
|
|||||||
import { cached } from "@ark/util";
|
import { cached } from "@ark/util";
|
||||||
|
import { appendFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
import { Octokit } from "@octokit/rest";
|
import { Octokit } from "@octokit/rest";
|
||||||
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
||||||
import type { FastMCP, Tool } from "fastmcp";
|
import type { FastMCP, Tool } from "fastmcp";
|
||||||
@@ -30,7 +32,90 @@ export interface McpContext extends RepoContext {
|
|||||||
octokit: Octokit;
|
octokit: Octokit;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const tool = <const params>(tool: Tool<any, StandardSchemaV1<params>>) => 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 = <const params>(toolDef: Tool<any, StandardSchemaV1<params>>) => {
|
||||||
|
// 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<any, any>[]) => {
|
export const addTools = (server: FastMCP, tools: Tool<any, any>[]) => {
|
||||||
for (const tool of tools) {
|
for (const tool of tools) {
|
||||||
|
|||||||
Reference in New Issue
Block a user