Skip to content

Commit 318f944

Browse files
committed
feat: Add Cursor integration guide for GitHub MCP governance via hooks
1 parent f0433a2 commit 318f944

2 files changed

Lines changed: 327 additions & 0 deletions

File tree

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

0 commit comments

Comments
 (0)