Skip to content

Commit 87897c1

Browse files
authored
Merge pull request #36 from agentcontrol/feat/cursor-mcp-integration-guide
feat: Add Cursor integration guide for GitHub MCP governance via hooks
2 parents 144bd12 + 1b7eb03 commit 87897c1

3 files changed

Lines changed: 333 additions & 0 deletions

File tree

.vale/styles/config/vocabularies/AgentControl-Vocab/accept.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ args
1010
[C,c]hatbot
1111
[C,c]hatbots
1212
Clickhouse
13+
Cursor
14+
MCP
15+
PAT
16+
[S,s]tderr
17+
[S,s]tdin
18+
[S,s]tdout
19+
[S,s]tdlib
20+
[S,s]ubprocesses
1321
[C,c]onfig
1422
CrewAI
1523
CTEs

docs.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,12 @@
7070
"integrations/langchain"
7171
]
7272
},
73+
{
74+
"group": "Third-Party Agents",
75+
"pages": [
76+
"third-party-agents/cursor"
77+
]
78+
},
7379
{
7480
"group": "Examples",
7581
"pages": [
@@ -198,6 +204,12 @@
198204
"integrations/strands",
199205
"integrations/google-adk"
200206
]
207+
},
208+
{
209+
"group": "Third-Party Agents",
210+
"pages": [
211+
"third-party-agents/cursor"
212+
]
201213
}
202214
]
203215
}

third-party-agents/cursor.mdx

Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
---
2+
title: Cursor
3+
description: Govern GitHub MCP tool calls from the Cursor agent using Agent Control
4+
---
5+
6+
[Cursor hooks](https://cursor.com/docs/agent/hooks) run external commands around the agent loop. This guide wires `beforeMCPExecution` to Agent Control's evaluation API to govern what the Cursor agent can do through the [GitHub MCP server](https://github.com/github/github-mcp-server).
7+
8+
Cursor has a built-in MCP allowlist, but it is per-machine and per-developer; there is no central place to manage policy across an organization. Agent Control adds that central layer, giving you one place to manage rules for Cursor, your own agents, and any other Agent Control-integrated tool.
9+
10+
By the end of this guide you will have:
11+
12+
- A hook script that evaluates every GitHub MCP call against Agent Control before it runs
13+
- A control that blocks write operations while leaving read tools open
14+
- A working end-to-end test you can trigger with a natural language prompt in Cursor
15+
16+
## How it works
17+
18+
```mermaid
19+
flowchart LR
20+
subgraph Cursor
21+
H[beforeMCPExecution]
22+
end
23+
subgraph Local
24+
S[ac_evaluate.py]
25+
end
26+
subgraph AgentControl[Agent Control server]
27+
E["/api/v1/evaluation"]
28+
C[Controls for cursor-agent]
29+
end
30+
H -->|JSON stdin| S
31+
S -->|agent_name + step| E
32+
E --> C
33+
E -->|is_safe + matches| S
34+
S -->|permission JSON stdout| H
35+
```
36+
37+
Cursor passes the hook payload as JSON on stdin. The script maps it to a **tool step** and calls `POST /api/v1/evaluation`. Agent Control evaluates the step against the controls linked to your `cursor-agent` and returns `is_safe`. The script writes `{"permission": "allow"}` or `{"permission": "deny"}` to stdout.
38+
39+
## Prerequisites
40+
41+
- [Cursor](https://cursor.com) installed (v0.48.0 or later)
42+
- Agent Control **server** running and reachable from your machine
43+
- **Python 3** on your PATH as `python3` (stdlib only — no extra packages needed)
44+
- For authenticated servers: an API key ([authentication guide](/how-to/enable-authentication))
45+
46+
### Set up GitHub MCP
47+
48+
If you haven't already, add the GitHub MCP server to `~/.cursor/mcp.json`:
49+
50+
```json
51+
{
52+
"mcpServers": {
53+
"github": {
54+
"url": "https://api.githubcopilot.com/mcp/",
55+
"headers": {
56+
"Authorization": "Bearer YOUR_GITHUB_PAT"
57+
}
58+
}
59+
}
60+
}
61+
```
62+
63+
Replace `YOUR_GITHUB_PAT` with a GitHub Personal Access Token that has repository permissions. Restart Cursor and confirm the server shows a green indicator under **Settings → Tools & Integrations → MCP Tools**.
64+
65+
See the [GitHub MCP installation guide](https://github.com/github/github-mcp-server/blob/main/docs/installation-guides/install-cursor.md) for more detail.
66+
67+
## 1. Create the hook script
68+
69+
Create `~/.cursor/hooks/ac_evaluate.py`:
70+
71+
```python
72+
#!/usr/bin/env python3
73+
"""
74+
Cursor hook: evaluates beforeMCPExecution events against Agent Control.
75+
Fail-open: any connection error returns {"permission": "allow"}.
76+
77+
Environment:
78+
AGENT_CONTROL_URL Server base URL (default: http://localhost:8000)
79+
AGENT_CONTROL_AGENT_NAME Agent name (default: cursor-agent)
80+
AGENT_CONTROL_API_KEY X-API-Key when server auth is enabled
81+
"""
82+
import json
83+
import os
84+
import sys
85+
import urllib.request
86+
from typing import Any
87+
88+
89+
def main() -> None:
90+
# Cursor passes the hook payload as JSON on stdin
91+
hook: dict[str, Any] = json.loads(sys.stdin.read())
92+
93+
server_url = os.environ.get("AGENT_CONTROL_URL", "http://localhost:8000").rstrip("/")
94+
agent_name = os.environ.get("AGENT_CONTROL_AGENT_NAME", "cursor-agent").lower()
95+
api_key = os.environ.get("AGENT_CONTROL_API_KEY", "")
96+
97+
# tool_input arrives as an escaped JSON string — parse it so controls can
98+
# evaluate nested fields like input.branch or input.owner
99+
raw_input = hook.get("tool_input", {})
100+
if isinstance(raw_input, str):
101+
try:
102+
raw_input = json.loads(raw_input)
103+
except (json.JSONDecodeError, ValueError):
104+
raw_input = {"raw": raw_input}
105+
106+
# Map the MCP hook to an Agent Control tool step
107+
step = {
108+
"type": "tool",
109+
"name": "mcp",
110+
"input": {
111+
"tool_name": hook.get("tool_name", ""),
112+
"tool_input": raw_input,
113+
"server_name": hook.get("serverName", ""),
114+
},
115+
}
116+
117+
payload = json.dumps({"agent_name": agent_name, "step": step, "stage": "pre"}).encode()
118+
headers = {"Content-Type": "application/json"}
119+
if api_key:
120+
headers["X-API-Key"] = api_key
121+
122+
req = urllib.request.Request(
123+
f"{server_url}/api/v1/evaluation",
124+
data=payload,
125+
headers=headers,
126+
method="POST",
127+
)
128+
129+
try:
130+
with urllib.request.urlopen(req, timeout=5) as resp:
131+
result = json.loads(resp.read())
132+
except Exception:
133+
# Fail-open: don't block the IDE if the server is unreachable
134+
print(json.dumps({"permission": "allow"}))
135+
return
136+
137+
if result.get("is_safe", True):
138+
print(json.dumps({"permission": "allow"}))
139+
else:
140+
# Surface the most specific reason available from the matched control
141+
matches = result.get("matches") or []
142+
first = matches[0] if matches else {}
143+
reason = (
144+
first.get("result", {}).get("message")
145+
or (first.get("control_name") and f"Blocked by {first['control_name']}")
146+
or result.get("reason")
147+
or "Blocked by Agent Control"
148+
)
149+
print(json.dumps({
150+
"permission": "deny",
151+
"user_message": f"[agent-control] {reason}",
152+
"agent_message": f"[agent-control] {reason}",
153+
}))
154+
155+
156+
if __name__ == "__main__":
157+
main()
158+
```
159+
160+
## 2. Register the hook
161+
162+
Create `~/.cursor/hooks.json` — Cursor automatically picks this up, no additional registration needed:
163+
164+
```json
165+
{
166+
"version": 1,
167+
"hooks": {
168+
"beforeMCPExecution": [
169+
{ "command": "python3 hooks/ac_evaluate.py", "timeout": 5 }
170+
]
171+
}
172+
}
173+
```
174+
175+
<Tip>
176+
For **team or repo-specific** behavior, use project hooks: `<project>/.cursor/hooks.json` with paths relative to the project root (e.g. `.cursor/hooks/ac_evaluate.py`).
177+
</Tip>
178+
179+
## 3. Register the agent and control
180+
181+
Run these three API calls once to set up Agent Control. Replace `http://localhost:8000` with your server URL.
182+
183+
**Register the agent:**
184+
185+
```bash
186+
# Declares cursor-agent and the mcp step type it can execute
187+
curl -X POST http://localhost:8000/api/v1/agents/initAgent \
188+
-H "Content-Type: application/json" \
189+
-d '{
190+
"agent": {
191+
"agent_name": "cursor-agent",
192+
"agent_description": "Cursor IDE agent",
193+
"agent_version": "1.0.0"
194+
},
195+
"steps": [{ "type": "tool", "name": "mcp" }],
196+
"evaluators": [],
197+
"conflict_mode": "overwrite"
198+
}'
199+
```
200+
201+
**Create the control:**
202+
203+
```bash
204+
# Blocks GitHub MCP write tools — any match on tool_name returns is_safe: false
205+
# Read tools (get_file_contents, list_pull_requests, search_code, etc.) pass through
206+
curl -X PUT http://localhost:8000/api/v1/controls \
207+
-H "Content-Type: application/json" \
208+
-d '{
209+
"name": "cursor-github-block-writes",
210+
"data": {
211+
"description": "Block GitHub MCP write operations from the Cursor agent",
212+
"enabled": true,
213+
"execution": "server",
214+
"scope": {
215+
"step_types": ["tool"],
216+
"step_names": ["mcp"],
217+
"stages": ["pre"]
218+
},
219+
"condition": {
220+
"selector": { "path": "input.tool_name" },
221+
"evaluator": {
222+
"name": "list",
223+
"config": {
224+
"values": [
225+
"push_files",
226+
"create_or_update_file",
227+
"delete_file",
228+
"merge_pull_request",
229+
"create_pull_request",
230+
"create_branch",
231+
"delete_branch"
232+
],
233+
"logic": "any",
234+
"match_on": "match",
235+
"match_mode": "exact",
236+
"case_sensitive": false
237+
}
238+
}
239+
},
240+
"action": { "decision": "deny" }
241+
}
242+
}'
243+
```
244+
245+
The response includes the new control's `control_id`. Copy it and use it in the next step.
246+
247+
**Link the control to the agent:**
248+
249+
```bash
250+
# Associates the control with cursor-agent so it is enforced on evaluation
251+
curl -X POST http://localhost:8000/api/v1/agents/cursor-agent/controls/{control_id}
252+
```
253+
254+
<Warning>
255+
Creating controls and linking them to an agent requires an **admin** API key when authentication is enabled. Add `-H "X-API-Key: your-admin-key"` to the curl commands above.
256+
</Warning>
257+
258+
## 4. Set environment variables
259+
260+
Hook subprocesses inherit the environment of the Cursor app. On macOS, set these in `~/.zshenv` (not `~/.zshrc`) so GUI apps pick them up:
261+
262+
```bash
263+
export AGENT_CONTROL_AGENT_NAME="cursor-agent"
264+
export AGENT_CONTROL_URL="http://localhost:8000"
265+
# If authentication is enabled:
266+
export AGENT_CONTROL_API_KEY="your-api-key"
267+
```
268+
269+
**Restart Cursor after updating env** — hooks and environment variables are only picked up on launch.
270+
271+
<Warning>
272+
Before restarting, make sure your Agent Control server is running. Once hooks are active, every MCP call the agent attempts will be evaluated — confirm the server is up first with `curl http://localhost:8000/health`.
273+
</Warning>
274+
275+
| Variable | Purpose |
276+
|----------|---------|
277+
| `AGENT_CONTROL_AGENT_NAME` | Must match the agent name registered above (default: `cursor-agent`) |
278+
| `AGENT_CONTROL_URL` | Server base URL (default: `http://localhost:8000`) |
279+
| `AGENT_CONTROL_API_KEY` | `X-API-Key` when server authentication is enabled |
280+
281+
## 5. Test it
282+
283+
**Verify the plumbing first** by piping a sample payload directly to the script:
284+
285+
```bash
286+
# Should return {"permission": "deny", ...}
287+
echo '{"tool_name":"push_files","tool_input":"{}","serverName":"github"}' \
288+
| python3 ~/.cursor/hooks/ac_evaluate.py
289+
290+
# Should return {"permission": "allow"}
291+
echo '{"tool_name":"list_pull_requests","tool_input":"{}","serverName":"github"}' \
292+
| python3 ~/.cursor/hooks/ac_evaluate.py
293+
```
294+
295+
**Then try it end-to-end in Cursor.** Ask the agent something that would trigger a write operation:
296+
297+
> "Create a pull request for these changes using the GitHub MCP."
298+
299+
Agent Control will deny the `create_pull_request` call
300+
301+
For comparison, a read request like "show me the open pull requests" will call `list_pull_requests` and pass through without interruption.
302+
303+
## Debugging
304+
305+
- Cursor **Settings → Hooks** shows a live output channel with stderr from hook scripts.
306+
- Verify the agent has controls linked: `GET /api/v1/agents/cursor-agent/controls`
307+
- Test the evaluation endpoint directly with `curl` before involving Cursor.
308+
309+
## Related documentation
310+
311+
- [Enable authentication](/how-to/enable-authentication)
312+
- [API reference — evaluation](/core/reference)
313+
- [Controls concept](/concepts/controls)

0 commit comments

Comments
 (0)