Skip to main content
Glama

Cursor Agent MCP Server

by sailay1996
server.js16.1 kB
// MCP wrapper server for cursor-agent CLI // Exposes multiple tools (chat/edit/analyze/search/plan/raw + legacy run) for better discoverability. // Start via MCP config (stdio). Requires Node 18+. import { z } from 'zod'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { spawn } from 'node:child_process'; import process from 'node:process'; // Tool input schema const RUN_SCHEMA = z.object({ prompt: z.string().min(1, 'prompt is required'), output_format: z.enum(['text', 'json', 'markdown']).default('text'), extra_args: z.array(z.string()).optional(), cwd: z.string().optional(), // Optional override for the executable path if not on PATH executable: z.string().optional(), // Optional model and force for parity with other tools/env overrides model: z.string().optional(), force: z.boolean().optional(), }); // Resolve the executable path for cursor-agent function resolveExecutable(explicit) { if (explicit && explicit.trim()) return explicit.trim(); if (process.env.CURSOR_AGENT_PATH && process.env.CURSOR_AGENT_PATH.trim()) { return process.env.CURSOR_AGENT_PATH.trim(); } // default assumes "cursor-agent" is on PATH return 'cursor-agent'; } /** * Internal executor that spawns cursor-agent with provided argv and common options. * Adds --print and --output-format, handles env/model/force, timeouts and idle kill. */ async function invokeCursorAgent({ argv, output_format = 'text', cwd, executable, model, force, print = true }) { const cmd = resolveExecutable(executable); // Compute model/force from args/env const userArgs = [...(argv ?? [])]; const hasModelFlag = userArgs.some((a) => a === '-m' || a === '--model' || /^(?:-m=|--model=)/.test(String(a))); const envModel = process.env.CURSOR_AGENT_MODEL && process.env.CURSOR_AGENT_MODEL.trim(); const effectiveModel = model?.trim?.() || envModel; const hasForceFlag = userArgs.some((a) => a === '-f' || a === '--force'); const envForce = (() => { const v = (process.env.CURSOR_AGENT_FORCE || '').toLowerCase(); return v === '1' || v === 'true' || v === 'yes' || v === 'on'; })(); const effectiveForce = typeof force === 'boolean' ? force : envForce; const finalArgv = [ ...(print ? ['--print', '--output-format', output_format] : []), ...userArgs, ...(hasForceFlag || !effectiveForce ? [] : ['-f']), ...(hasModelFlag || !effectiveModel ? [] : ['-m', effectiveModel]), ]; return new Promise((resolve) => { let settled = false; let out = ''; let err = ''; let idleTimer = null; let killedByIdle = false; const cleanup = () => { if (mainTimer) clearTimeout(mainTimer); if (idleTimer) clearTimeout(idleTimer); }; if (process.env.DEBUG_CURSOR_MCP === '1') { try { console.error('[cursor-mcp] spawn:', cmd, ...finalArgv); } catch {} } const child = spawn(cmd, finalArgv, { shell: false, // safer across platforms; rely on PATH/PATHEXT cwd: cwd || process.cwd(), env: process.env, }); try { child.stdin?.end(); } catch {} const idleMs = Number.parseInt(process.env.CURSOR_AGENT_IDLE_EXIT_MS || '0', 10); const scheduleIdleKill = () => { if (!Number.isFinite(idleMs) || idleMs <= 0) return; if (idleTimer) clearTimeout(idleTimer); idleTimer = setTimeout(() => { killedByIdle = true; try { child.kill('SIGKILL'); } catch {} }, idleMs); }; child.stdout.on('data', (d) => { out += d.toString(); scheduleIdleKill(); }); child.stderr.on('data', (d) => { err += d.toString(); }); child.on('error', (e) => { if (settled) return; settled = true; cleanup(); if (process.env.DEBUG_CURSOR_MCP === '1') { try { console.error('[cursor-mcp] error:', e); } catch {} } const msg = `Failed to start "${cmd}": ${e?.message || e}\n` + `Args: ${JSON.stringify(finalArgv)}\n` + (process.env.CURSOR_AGENT_PATH ? `CURSOR_AGENT_PATH=${process.env.CURSOR_AGENT_PATH}\n` : ''); resolve({ content: [{ type: 'text', text: msg }], isError: true }); }); const defaultTimeout = 30000; const timeoutMs = Number.parseInt(process.env.CURSOR_AGENT_TIMEOUT_MS || String(defaultTimeout), 10); const mainTimer = setTimeout(() => { try { child.kill('SIGKILL'); } catch {} if (settled) return; settled = true; cleanup(); resolve({ content: [{ type: 'text', text: `cursor-agent timed out after ${Number.isFinite(timeoutMs) ? timeoutMs : defaultTimeout}ms` }], isError: true, }); }, Number.isFinite(timeoutMs) ? timeoutMs : defaultTimeout); child.on('close', (code) => { if (settled) return; settled = true; cleanup(); if (process.env.DEBUG_CURSOR_MCP === '1') { try { console.error('[cursor-mcp] exit:', code, 'stdout bytes=', out.length, 'stderr bytes=', err.length); } catch {} } if (code === 0 || (killedByIdle && out)) { resolve({ content: [{ type: 'text', text: out || '(no output)' }] }); } else { resolve({ content: [{ type: 'text', text: `cursor-agent exited with code ${code}\n${err || out || '(no output)'}` }], isError: true, }); } }); }); } // Back-compat: single-shot run by prompt as positional argument. // Accepts either a flat args object or an object with an "arguments" field (some hosts). async function runCursorAgent(input) { const source = (input && typeof input === 'object' && input.arguments && typeof input.prompt === 'undefined') ? input.arguments : input; const { prompt, output_format = 'text', extra_args, cwd, executable, model, force, } = source || {}; const argv = [...(extra_args ?? []), String(prompt)]; const usedPrompt = argv.length ? String(argv[argv.length - 1]) : ''; // Optional prompt echo and debug diagnostics if (process.env.DEBUG_CURSOR_MCP === '1') { try { const preview = usedPrompt.slice(0, 400).replace(/\n/g, '\\n'); console.error('[cursor-mcp] prompt:', preview); if (extra_args?.length) console.error('[cursor-mcp] extra_args:', JSON.stringify(extra_args)); if (model) console.error('[cursor-mcp] model:', model); if (typeof force === 'boolean') console.error('[cursor-mcp] force:', String(force)); } catch {} } const result = await invokeCursorAgent({ argv, output_format, cwd, executable, model, force }); // Echo prompt either when env is set or when caller provided echo_prompt: true (if host forwards unknown args it's fine) const echoEnabled = process.env.CURSOR_AGENT_ECHO_PROMPT === '1' || source?.echo_prompt === true; if (echoEnabled) { const text = `Prompt used:\n${usedPrompt}`; const content = Array.isArray(result?.content) ? result.content : []; return { ...result, content: [{ type: 'text', text }, ...content] }; } return result; } /** * Create MCP server and register a suite of cursor-agent tools. * We expose multiple verbs for better discoverability in hosts (chat/edit/analyze/search/plan), * plus the legacy cursor_agent_run for back-compat and a raw escape hatch. */ const server = new McpServer( { name: 'cursor-agent', version: '1.1.0', description: 'MCP wrapper for cursor-agent CLI (multi-tool: chat/edit/analyze/search/plan/raw)', }, { instructions: [ 'Tools:', '- cursor_agent_chat: chat with a prompt; optional model/force/format.', '- cursor_agent_edit_file: prompt-based file edit wrapper; you provide file and instruction.', '- cursor_agent_analyze_files: prompt-based analysis of one or more paths.', '- cursor_agent_search_repo: prompt-based code search with include/exclude globs.', '- cursor_agent_plan_task: prompt-based planning given a goal and optional constraints.', '- cursor_agent_raw: pass raw argv directly to cursor-agent; set print=false to avoid implicit --print.', '- cursor_agent_run: legacy single-shot chat (prompt as positional).', ].join(' '), }, ); // Common shape used by multiple schemas const COMMON = { output_format: z.enum(['text', 'json', 'markdown']).default('text'), extra_args: z.array(z.string()).optional(), cwd: z.string().optional(), executable: z.string().optional(), model: z.string().optional(), force: z.boolean().optional(), // When true, the server will prepend the effective prompt to the tool output (useful for Claude debugging) echo_prompt: z.boolean().optional(), }; // Schemas const CHAT_SCHEMA = z.object({ prompt: z.string().min(1, 'prompt is required'), ...COMMON, }); const EDIT_FILE_SCHEMA = z.object({ file: z.string().min(1, 'file is required'), instruction: z.string().min(1, 'instruction is required'), apply: z.boolean().optional(), dry_run: z.boolean().optional(), // optional free-form prompt to pass if the CLI supports one prompt: z.string().optional(), ...COMMON, }); const ANALYZE_FILES_SCHEMA = z.object({ paths: z.union([z.string().min(1), z.array(z.string().min(1)).min(1)]), prompt: z.string().optional(), ...COMMON, }); const SEARCH_REPO_SCHEMA = z.object({ query: z.string().min(1, 'query is required'), include: z.union([z.string(), z.array(z.string())]).optional(), exclude: z.union([z.string(), z.array(z.string())]).optional(), ...COMMON, }); const PLAN_TASK_SCHEMA = z.object({ goal: z.string().min(1, 'goal is required'), constraints: z.array(z.string()).optional(), ...COMMON, }); const RAW_SCHEMA = z.object({ // raw argv to pass after common flags; e.g., ["--help"] or ["subcmd","--flag"] argv: z.array(z.string()).min(1, 'argv must contain at least one element'), print: z.boolean().optional(), ...COMMON, }); // Tools server.tool( 'cursor_agent_chat', 'Chat with cursor-agent using a prompt and optional model/force/output_format.', CHAT_SCHEMA.shape, async (args) => { try { // Normalize prompt in case the host nests under "arguments" const prompt = (args && typeof args === 'object' && 'prompt' in args ? args.prompt : undefined) ?? (args && typeof args === 'object' && args.arguments && typeof args.arguments === 'object' ? args.arguments.prompt : undefined); const flat = { ...(args && typeof args === 'object' && args.arguments && typeof args.arguments === 'object' ? args.arguments : args), prompt, }; return await runCursorAgent(flat); } catch (e) { return { content: [{ type: 'text', text: `Invalid params: ${e?.message || e}` }], isError: true }; } }, ); server.tool( 'cursor_agent_edit_file', 'Edit a file with an instruction. Prompt-based wrapper; no CLI subcommand required.', EDIT_FILE_SCHEMA.shape, async (args) => { try { const { file, instruction, apply, dry_run, prompt, output_format, cwd, executable, model, force, extra_args } = args; const composedPrompt = `Edit the repository file:\n` + `- File: ${String(file)}\n` + `- Instruction: ${String(instruction)}\n` + (apply ? `- Apply changes if safe.\n` : `- Propose a patch/diff without applying.\n`) + (dry_run ? `- Treat as dry-run; do not write to disk.\n` : ``) + (prompt ? `- Additional context: ${String(prompt)}\n` : ``); return await runCursorAgent({ prompt: composedPrompt, output_format, extra_args, cwd, executable, model, force }); } catch (e) { return { content: [{ type: 'text', text: `Invalid params: ${e?.message || e}` }], isError: true }; } }, ); server.tool( 'cursor_agent_analyze_files', 'Analyze one or more paths; optional prompt. Prompt-based wrapper.', ANALYZE_FILES_SCHEMA.shape, async (args) => { try { const { paths, prompt, output_format, cwd, executable, model, force, extra_args } = args; const list = Array.isArray(paths) ? paths : [paths]; const composedPrompt = `Analyze the following paths in the repository:\n` + list.map((p) => `- ${String(p)}`).join('\n') + '\n' + (prompt ? `Additional prompt: ${String(prompt)}\n` : ''); return await runCursorAgent({ prompt: composedPrompt, output_format, extra_args, cwd, executable, model, force }); } catch (e) { return { content: [{ type: 'text', text: `Invalid params: ${e?.message || e}` }], isError: true }; } }, ); server.tool( 'cursor_agent_search_repo', 'Search repository code with include/exclude patterns. Prompt-based wrapper.', SEARCH_REPO_SCHEMA.shape, async (args) => { try { const { query, include, exclude, output_format, cwd, executable, model, force, extra_args } = args; const inc = include == null ? [] : (Array.isArray(include) ? include : [include]); const exc = exclude == null ? [] : (Array.isArray(exclude) ? exclude : [exclude]); const composedPrompt = `Search the repository for occurrences relevant to:\n` + `- Query: ${String(query)}\n` + (inc.length ? `- Include globs:\n${inc.map((p)=>` - ${String(p)}`).join('\n')}\n` : '') + (exc.length ? `- Exclude globs:\n${exc.map((p)=>` - ${String(p)}`).join('\n')}\n` : '') + `Return concise findings with file paths and line references.`; return await runCursorAgent({ prompt: composedPrompt, output_format, extra_args, cwd, executable, model, force }); } catch (e) { return { content: [{ type: 'text', text: `Invalid params: ${e?.message || e}` }], isError: true }; } }, ); server.tool( 'cursor_agent_plan_task', 'Generate a plan for a goal with optional constraints. Prompt-based wrapper.', PLAN_TASK_SCHEMA.shape, async (args) => { try { const { goal, constraints, output_format, cwd, executable, model, force, extra_args } = args; const cons = constraints ?? []; const composedPrompt = `Create a step-by-step plan to accomplish the following goal:\n` + `- Goal: ${String(goal)}\n` + (cons.length ? `- Constraints:\n${cons.map((c)=>` - ${String(c)}`).join('\n')}\n` : '') + `Provide a numbered list of actions.`; return await runCursorAgent({ prompt: composedPrompt, output_format, extra_args, cwd, executable, model, force }); } catch (e) { return { content: [{ type: 'text', text: `Invalid params: ${e?.message || e}` }], isError: true }; } }, ); // Raw escape hatch for power-users and forward compatibility server.tool( 'cursor_agent_raw', 'Advanced: provide raw argv array to pass after common flags (e.g., ["search","--query","foo"]).', RAW_SCHEMA.shape, async (args) => { try { const { argv, output_format, cwd, executable, model, force } = args; // For raw calls we disable implicit --print to allow commands like "--help" return await invokeCursorAgent({ argv, output_format, cwd, executable, model, force, print: false }); } catch (e) { return { content: [{ type: 'text', text: `Invalid params: ${e?.message || e}` }], isError: true }; } }, ); // Legacy single-shot prompt tool retained for compatibility server.tool( 'cursor_agent_run', 'Run cursor-agent with a prompt and desired output format (legacy single-shot).', RUN_SCHEMA.shape, async (args) => { try { return await runCursorAgent(args); } catch (e) { return { content: [{ type: 'text', text: `Invalid params: ${e?.message || e}` }], isError: true }; } }, ); // Connect using stdio transport const transport = new StdioServerTransport(); server.connect(transport).catch((e) => { console.error('MCP server failed to start:', e); process.exit(1); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sailay1996/cursor-agent-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server