|
| 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