Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ venv*/
# Logs
*.log

# Node
node_modules/

#Others
.DS_Store
*.skill

.cursor/rules/
13 changes: 13 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"mcpServers": {
"cursor-agent-orchestrator": {
"command": "node",
"args": [
"/Users/thesunkid/Desktop/code/feedback-mcp/cursor-agent-mcp/orchestrator-mcp/server.js"
],
"env": {
"BRIDGE_SESSION_DIR": "/tmp/cursor-bridge-session"
}
}
}
}
195 changes: 195 additions & 0 deletions cursor-agent-mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
# Cursor Agent MCP — Bidirectional Claude Code ↔ Cursor CLI Orchestration

MCP-based system for spawning a `cursor-agent` as a background subagent and communicating with it bidirectionally. One cursor-agent process, many turns, fully non-blocking.

## Architecture

```
┌──────────────────────┐ MCP tools ┌───────────────────────┐
│ Claude Code │◄─────────────────────────►│ Orchestrator MCP │
│ (master agent) │ cursor_agent_spawn/ │ orchestrator-mcp/ │
│ │ check/reply/status/ │ server.js │
│ │ result/kill │ │
└──────────────────────┘ └───────┬───────────────┘
│ file IPC
│ /tmp/cursor-bridge-session/
│ question_N.json ↔ answer_N.json
┌──────────────────────┐ MCP tool ┌───────┴───────────────┐
│ cursor-agent │──────────────────────────►│ Bridge MCP │
│ (subagent, 1 proc) │ report_to_orchestrator │ bridge-mcp/ │
│ --print --yolo │ (blocks until answered) │ server.js │
└──────────────────────┘ └───────────────────────┘
```

### Flow

1. Claude Code calls `cursor_agent_spawn(task, model)` → orchestrator MCP spawns `cursor-agent --print --yolo` in background
2. cursor-agent works on the task. When it needs to communicate, it calls `report_to_orchestrator` (bridge MCP tool)
3. Bridge MCP writes `question_N.json` to the session dir, then **blocks** polling for `answer_N.json`
4. Claude Code calls `cursor_agent_check()` → sees the question
5. Claude Code calls `cursor_agent_reply(answer)` → writes `answer_N.json`
6. Bridge MCP reads the answer, unblocks, returns it to cursor-agent
7. cursor-agent continues working, repeats from step 2
8. When done, Claude Code calls `cursor_agent_result()` for final output

## Components

### 1. Orchestrator MCP (`orchestrator-mcp/server.js`)

**Claude Code connects to this.** Provides 6 tools:

| Tool | Description |
|------|-------------|
| `cursor_agent_spawn` | Spawn background cursor-agent with task + model. Auto-prepends bridge protocol. |
| `cursor_agent_check` | Check for pending message from cursor-agent |
| `cursor_agent_reply` | Send reply to cursor-agent's question |
| `cursor_agent_status` | Get agent status: working / waiting_for_reply / completed |
| `cursor_agent_result` | Get agent's final stdout output |
| `cursor_agent_kill` | Force-terminate the agent |

Config for Claude Code (`.mcp.json` at project root):
```json
{
"mcpServers": {
"cursor-agent-orchestrator": {
"command": "node",
"args": ["/path/to/cursor-agent-mcp/orchestrator-mcp/server.js"],
"env": { "BRIDGE_SESSION_DIR": "/tmp/cursor-bridge-session" }
}
}
}
```

### 2. Bridge MCP (`bridge-mcp/server.js`)

**cursor-agent connects to this.** Single tool: `report_to_orchestrator(message)`.

When called, writes a question file to the session dir, then blocks polling for an answer file. Returns the answer to cursor-agent when it appears.

Config for cursor-agent (`~/.cursor/mcp.json`):
```json
{
"mcpServers": {
"orchestrator-bridge": {
"command": "node",
"args": ["/path/to/cursor-agent-mcp/bridge-mcp/server.js"],
"env": {
"BRIDGE_SESSION_DIR": "/tmp/cursor-bridge-session",
"BRIDGE_POLL_MS": "500",
"BRIDGE_TIMEOUT_MS": "300000"
}
}
}
}
```

Must be enabled: `cursor-agent mcp enable orchestrator-bridge`

### 3. Orchestrator CLI (`orchestrator.js`)

Standalone CLI wrapper for the same file IPC, useful for testing or Bash-based workflows:

```bash
node orchestrator.js spawn "task" --model composer-1
node orchestrator.js check # pending question?
node orchestrator.js reply "answer"
node orchestrator.js status # working/waiting/completed
node orchestrator.js result # final output
node orchestrator.js kill # terminate
```

### 4. Legacy One-Shot Tools (`server.js`)

The original single-shot MCP tools (`cursor_agent_chat`, `cursor_agent_edit_file`, etc.) for fire-and-forget delegation. Still work but don't support bidirectional communication.

## Setup

### Prerequisites

- Node.js 18+
- `cursor-agent` CLI installed and authenticated (`cursor-agent status`)

### Install

```bash
cd cursor-agent-mcp
npm install
```

### Configure bridge MCP for cursor-agent

Add to `~/.cursor/mcp.json` (see config above), then:

```bash
cursor-agent mcp enable orchestrator-bridge
cursor-agent mcp list-tools orchestrator-bridge
# Should show: report_to_orchestrator (message)
```

### Configure orchestrator MCP for Claude Code

Add `.mcp.json` to the project root (see config above). Restart Claude Code to load.

## Key Design Decisions

- **One process per task.** cursor-agent runs as a single `--print --yolo` process. Its agentic loop calls `report_to_orchestrator` multiple times internally.
- **File-based IPC.** Simple, debuggable, no sockets. Question/answer JSON files in a shared directory.
- **Blocking bridge.** The bridge MCP blocks until the orchestrator answers. cursor-agent waits naturally — no polling from its side.
- **Non-blocking orchestrator.** Claude Code spawns the agent in background (`detached: true`) and checks on it whenever convenient.
- **Protocol preamble auto-injected.** `cursor_agent_spawn` automatically prepends the bridge communication rules to the task prompt. The user just provides the task.
- **`--model` not `-m`.** cursor-agent's `-m` short flag is broken. Always use `--model`.
- **`--yolo` / `-f` for trust.** Required to skip workspace trust prompts in non-interactive mode.

## Testing

Quick smoke test of the bridge:

```bash
# Terminal 1: Spawn agent
node orchestrator.js spawn "Ask me what to build via report_to_orchestrator" --model composer-1

# Terminal 2: Watch for questions and answer
node orchestrator.js check
node orchestrator.js reply "Build a hello world function"
node orchestrator.js check
node orchestrator.js reply "Looks good, stop"
node orchestrator.js result
```

Full automated test (5 turns, 1 process):

```bash
node test_5turn_bridge.mjs
```

## File Structure

```
cursor-agent-mcp/
├── server.js # Legacy one-shot MCP tools (chat/edit/analyze/search/plan)
├── orchestrator.js # CLI wrapper for file IPC (spawn/check/reply/status/result/kill)
├── package.json
├── bridge-mcp/
│ └── server.js # Bridge MCP: report_to_orchestrator (cursor-agent side)
├── orchestrator-mcp/
│ └── server.js # Orchestrator MCP: cursor_agent_* tools (Claude Code side)
├── hooks/
│ ├── post-tool-use.js # PostToolUse hook for auto-discovering pending questions
│ └── README.md
├── docs/
│ ├── ARCHITECTURE.md # Detailed architecture documentation
│ └── TESTING.md # Comprehensive testing guide
├── test_5turn_bridge.mjs # 5-turn single-process e2e test
├── test_3turn.mjs # 3-turn session test
├── test_session_e2e.mjs # Session tools e2e test
├── test_session_feedback.mjs # 3-round feedback test
└── test_client.mjs # Legacy smoke test client
```

## Future Improvements

- **Auto-answer mode.** Claude Code automatically decides answers without manual `reply` calls — full autonomous delegation.
- **Multiple concurrent agents.** Session-scoped IPC dirs to support parallel subagents.
- **Structured output.** Parse cursor-agent's stream-json output for richer status reporting.
- **Webhook/push notification.** Replace file polling with an HTTP callback or Unix socket for instant delivery.
- **Timeout + retry.** Auto-retry if bridge MCP times out, with exponential backoff.
101 changes: 101 additions & 0 deletions cursor-agent-mcp/bridge-mcp/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* Bridge MCP Server — file-based blocking IPC for orchestrator communication.
*
* Exposes a single tool `report_to_orchestrator` that cursor-agent calls
* when it needs to communicate with the orchestrating agent (e.g. Claude Code).
*
* Flow:
* 1. cursor-agent calls report_to_orchestrator(message)
* 2. This server writes the message to a question file
* 3. It polls for an answer file (blocks until one appears)
* 4. Returns the answer to cursor-agent
*
* The orchestrator watches for question files and writes answer files.
*
* Env:
* BRIDGE_SESSION_DIR — directory for IPC files (required, set by orchestrator)
* BRIDGE_POLL_MS — poll interval in ms (default: 500)
* BRIDGE_TIMEOUT_MS — max wait time in ms (default: 300000 = 5 min)
*/

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import { writeFileSync, readFileSync, unlinkSync, existsSync } from 'node:fs';
import { join } from 'node:path';

const SESSION_DIR = process.env.BRIDGE_SESSION_DIR;
if (!SESSION_DIR) {
console.error('BRIDGE_SESSION_DIR env is required');
process.exit(1);
}

const POLL_MS = parseInt(process.env.BRIDGE_POLL_MS || '500', 10);
const TIMEOUT_MS = parseInt(process.env.BRIDGE_TIMEOUT_MS || '300000', 10);

let turnCounter = 0;

function sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}

const server = new McpServer(
{ name: 'orchestrator-bridge', version: '1.0.0' },
{
instructions: [
'This MCP provides a single tool: report_to_orchestrator.',
'Use it to send messages to the orchestrating agent and receive replies.',
'Call it whenever you need to:',
'- Ask a clarifying question',
'- Report progress or intermediate results',
'- Request feedback on your work',
'- Deliver your final result',
'The orchestrator will reply through this same channel.',
'Always wait for the orchestrator reply before continuing.',
].join(' '),
}
);

server.tool(
'report_to_orchestrator',
'Send a message to the orchestrating agent and wait for their reply. Use this for questions, progress updates, intermediate results, or final deliverables.',
{ message: z.string().min(1, 'message is required') },
async ({ message }) => {
turnCounter++;
const turn = turnCounter;

const questionFile = join(SESSION_DIR, `question_${turn}.json`);
const answerFile = join(SESSION_DIR, `answer_${turn}.json`);

// Write question
writeFileSync(questionFile, JSON.stringify({ turn, message, timestamp: Date.now() }), 'utf8');

// Poll for answer
const deadline = Date.now() + TIMEOUT_MS;
while (Date.now() < deadline) {
if (existsSync(answerFile)) {
try {
const data = JSON.parse(readFileSync(answerFile, 'utf8'));
// Clean up
try { unlinkSync(questionFile); } catch {}
try { unlinkSync(answerFile); } catch {}
return { content: [{ type: 'text', text: data.reply || '(empty reply)' }] };
} catch {
// File not fully written yet, retry
}
}
await sleep(POLL_MS);
}

return {
content: [{ type: 'text', text: `Orchestrator did not reply within ${TIMEOUT_MS}ms.` }],
isError: true,
};
}
);

const transport = new StdioServerTransport();
server.connect(transport).catch((e) => {
console.error('Bridge MCP failed to start:', e);
process.exit(1);
});
Loading