add Cursor, fix Gemini

This commit is contained in:
Shawn Morreau
2025-11-18 20:23:50 -05:00
committed by GitHub
11 changed files with 567 additions and 109 deletions
-3
View File
@@ -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;
+37 -30
View File
@@ -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 <name> --env KEY=value -- <command> [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 <name> --env KEY=value -- <command> [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`);
}
}
+130
View File
@@ -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");
}
+33 -28
View File
@@ -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 <name> <commandOrUrl> [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 <name> <commandOrUrl> [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`);
}
}
+2
View File
@@ -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;
+109 -22
View File
@@ -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<string, McpServerConfig>;
mcpServers: Record<string, McpStdioServerConfig>;
cliPath: string;
}
/**
* Parameters for configuring MCP servers
*/
export interface ConfigureMcpServersParams {
mcpServers: Record<string, McpStdioServerConfig>;
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<string, unknown>;
}
/**
* 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<string> {
}: InstallFromNpmTarballParams): Promise<string> {
// 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<string, unknown>;
};
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<string> {
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 = <const agent extends Agent>(agent: agent): agent => {
return agent;
};
/**
* Parameters for adding an MCP server to an agent
*/
export interface AddMcpServerParams {
serverName: string;
serverConfig: Extract<McpServerConfig, { command: string }>;
cliPath: string;
}
export type Agent = {
name: string;
inputKeys: string[];
install: () => Promise<string>;
addMcpServer: (params: AddMcpServerParams) => void;
run: (config: AgentConfig) => Promise<AgentResult>;
};
+183 -6
View File
@@ -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
+1 -1
View File
@@ -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
+1 -13
View File
@@ -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<MainResult> {
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,
+4 -5
View File
@@ -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<McpName, McpServerConfig>;
export type McpConfigs = Record<McpName, McpStdioServerConfig>;
export function createMcpConfigs(githubInstallationToken: string): McpConfigs {
const repoContext = parseRepoContext();
@@ -38,7 +38,7 @@ export function createMcpConfigs(githubInstallationToken: string): McpConfigs {
*/
export function forEachStdioMcpServer(
mcpServers: Record<string, McpServerConfig>,
handler: (serverName: string, serverConfig: Extract<McpServerConfig, { command: string }>) => 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<McpServerConfig, { command: string }>);
handler(serverName, serverConfig);
}
}
+67 -1
View File
@@ -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 = <const params>(tool: Tool<any, StandardSchemaV1<params>>) => 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 = <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>[]) => {
for (const tool of tools) {