Skip to main content
Glama
cursor.ts7.89 kB
import fs from 'fs'; import path from 'path'; import os from 'os'; import { promisify } from 'util'; import { execFile } from 'child_process'; import { setTimeout as delay } from 'timers/promises'; import { CursorAgentRequirement } from '../types'; import { AiAgentController, AgentPreparationResult } from './types'; import { CursorConfigSnapshot, ensureCursorConfig, expandHome, restoreCursorConfig, waitForCursorConnection, } from './cursorShared'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; const execFileAsync = promisify(execFile); const DEFAULT_CONFIG_PATH = path.join(os.homedir(), '.cursor', 'mcp.json'); const DEFAULT_SERVER_KEY = 'mcpx'; const DEFAULT_URL = 'http://127.0.0.1:9000/mcp'; const DEFAULT_TIMEOUT_SEC = 120; async function fileExists(file: string): Promise<boolean> { try { await fs.promises.access(file, fs.constants.F_OK); return true; } catch { return false; } } async function quitCursor(): Promise<void> { try { await execFileAsync('osascript', ['-e', 'tell application "Cursor" to quit']); } catch (err: any) { if (err?.code !== 1) { throw err; } } } async function isCursorRunning(): Promise<boolean> { try { await execFileAsync('pgrep', ['-x', 'Cursor']); return true; } catch (err: any) { if (err?.code === 1) return false; throw err; } } async function waitForCursorExit(timeoutMs: number): Promise<void> { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { if (!(await isCursorRunning())) return; await delay(500); } throw new Error('Cursor did not exit within the expected timeout'); } interface CursorState { configPath: string; projectConfigPath: string; serverKey: string; url: string; startupTimeoutSec: number; } export class CursorController implements AiAgentController { public readonly requirement: CursorAgentRequirement; private readonly state: CursorState; private configSnapshot?: CursorConfigSnapshot; private startedAgent = false; private useStub = false; private stubClient?: Client; private stubTransport?: SSEClientTransport; constructor(requirement: CursorAgentRequirement, state: CursorState) { this.requirement = requirement; this.state = state; } async prepare(): Promise<AgentPreparationResult> { if (process.platform !== 'darwin') { if (this.requirement.skipIfMissing ?? true) { console.warn('⚠️ Cursor tests require macOS for automation. Skipping scenario.'); return 'skip'; } throw new Error('Cursor tests require macOS for automation.'); } if (!(await this.detectInstallation())) { if (this.requirement.skipIfMissing ?? true) { console.warn('⚠️ Cursor not detected. Skipping scenario.'); return 'skip'; } throw new Error('Cursor app not detected at expected locations.'); } this.configSnapshot = await ensureCursorConfig({ configPath: this.state.configPath, projectConfigPath: this.state.projectConfigPath, serverKey: this.state.serverKey, url: this.state.url, }); if (await isCursorRunning()) { console.log('→ Cursor is running, requesting it to quit before the test'); await quitCursor(); await waitForCursorExit(10_000); } return 'ready'; } async start(): Promise<void> { let launched = false; try { console.log('→ Launching Cursor'); await execFileAsync('open', ['--hide', '--background', '-gj', '-a', 'Cursor']); this.startedAgent = true; launched = true; } catch (err) { const message = (err as Error)?.message ?? String(err); console.warn(`⚠️ Failed to launch Cursor app (${message}). Falling back to stub.`); } if (launched) { try { await waitForCursorConnection({ startupTimeoutSec: this.state.startupTimeoutSec }); return; } catch (err) { const message = (err as Error)?.message ?? String(err); console.warn( `⚠️ Cursor app did not connect within ${this.state.startupTimeoutSec}s (${message}). Falling back to stub.` ); try { await quitCursor(); } catch (quitErr) { console.warn( '⚠️ Failed to stop Cursor before stub fallback:', (quitErr as Error).message ); } await delay(1_000); } } await this.startStub(); } async cleanup(): Promise<void> { if (this.useStub) { await this.stopStub(); } if (this.startedAgent) { try { console.log('→ Stopping Cursor'); await quitCursor(); await waitForCursorExit(10_000); } catch (err) { console.warn('⚠️ Failed to stop Cursor:', (err as Error).message); } } if (this.configSnapshot) { await restoreCursorConfig(this.configSnapshot); } } private async startStub(): Promise<void> { console.log('→ Launching Cursor stub connection'); this.useStub = true; const headers: Record<string, string> = { 'x-lunar-consumer-tag': 'Cursor', 'user-agent': 'cursor-stub/1.0.0', }; const baseUrl = new URL(this.state.url); const sseUrl = new URL(baseUrl.toString()); sseUrl.pathname = '/sse'; const transport = new SSEClientTransport(sseUrl, { eventSourceInit: { fetch: async (url, init) => { const combined = new Headers(init?.headers); for (const [key, value] of Object.entries(headers)) { combined.set(key, value); } return fetch(url, { ...init, headers: combined }); }, }, requestInit: { headers }, }); transport.onerror = (error) => { console.warn('⚠️ Cursor stub transport error:', error.message ?? error); }; this.stubTransport = transport; const client = new Client({ name: 'Cursor Stub', version: '1.0.0' }); this.stubClient = client; try { await client.connect(transport); await waitForCursorConnection({ startupTimeoutSec: this.state.startupTimeoutSec }); } catch (err) { await this.stopStub(); throw err; } } private async stopStub(): Promise<void> { const client = this.stubClient; const transport = this.stubTransport; this.stubClient = undefined; this.stubTransport = undefined; if (!client && !transport) { return; } console.log('→ Stopping Cursor stub connection'); try { await client?.close(); } catch (err) { console.warn('⚠️ Failed to close Cursor stub client:', (err as Error).message); } try { await transport?.close(); } catch (err) { console.warn('⚠️ Failed to close Cursor stub transport:', (err as Error).message); } } private async detectInstallation(): Promise<boolean> { const candidates = [ '/Applications/Cursor.app', path.join(os.homedir(), 'Applications', 'Cursor.app'), path.dirname(this.state.configPath), ]; for (const candidate of candidates) { if (await fileExists(candidate)) return true; } return false; } } export function createCursorController(requirement: CursorAgentRequirement): CursorController { const configPath = expandHome(requirement.configPath ?? DEFAULT_CONFIG_PATH); const projectConfigPath = path.resolve('.cursor', 'mcp.json'); const serverKey = requirement.serverKey ?? DEFAULT_SERVER_KEY; const url = requirement.url ?? DEFAULT_URL; const startupTimeoutSec = requirement.startupTimeoutSec ?? DEFAULT_TIMEOUT_SEC; const state: CursorState = { configPath, projectConfigPath, serverKey, url, startupTimeoutSec, }; return new CursorController(requirement, state); }

Latest Blog Posts

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/TheLunarCompany/lunar'

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