Skip to content

Instantly share code, notes, and snippets.

@jcpowermac
Last active June 12, 2026 14:06
Show Gist options
  • Select an option

  • Save jcpowermac/f3191039cb58cbede817aa0d61cb94d9 to your computer and use it in GitHub Desktop.

Select an option

Save jcpowermac/f3191039cb58cbede817aa0d61cb94d9 to your computer and use it in GitHub Desktop.
Serena MCP + Cursor global setup (hooks, MCP config, user rules)

Serena + Cursor (global setup)

Make Cursor agents actually use Serena symbolic tools instead of defaulting to Read/Grep/SemanticSearch. Adapted from Serena's Claude Code client docs.

This is a user-global setup (~/.cursor/). It applies to every project you open in Cursor.

Prerequisites

  1. Install Serena (do not use marketplace/plugin installers):

    uv tool install --python 3.13 serena-agent
    serena init
  2. Verify CLI:

    which serena serena-hooks
    serena start-mcp-server --help
  3. Per-repo (once per project): initialize Serena in the repo root:

    cd your-repo
    serena project init   # creates .serena/project.yml

    Edit .serena/project.yml and set languages: for your stack (e.g. python, typescript).

Install (5 files + 1 user rule)

1. MCP server — ~/.cursor/mcp.json

Merge with any existing servers. If you already have other MCP entries, add the serena block only.

See mcp.json in this gist.

Replace /home/you/.local/bin/serena with the output of which serena. Use the full path — Cursor's MCP subprocess often does not inherit your shell PATH, so bare serena may fail even when it works in a terminal.

Key flags:

  • --context=ide — Serena defers file/shell tools to Cursor; exposes symbolic LSP tools (recommended for Cursor per Serena client docs)
  • --project-from-cwd — auto-detects .serena/project.yml in the opened workspace

2. Hook scripts — ~/.cursor/hooks/

Copy these four files into ~/.cursor/hooks/ and make the .sh files executable:

mkdir -p ~/.cursor/hooks
cp serena_bridge.py serena-*.sh ~/.cursor/hooks/
chmod +x ~/.cursor/hooks/serena-*.sh
File Role
serena_bridge.py Translates Cursor hook JSON ↔ Serena serena-hooks CLI
serena-activate.sh Session start: activate project + read Serena instructions
serena-remind.sh After repeated Read/Grep/SemanticSearch/Shell: nudge toward symbolic tools
serena-cleanup.sh Session end: clean hook state

3. Hooks manifest — ~/.cursor/hooks.json

See hooks.json. Merge with your existing hooks if you already have a hooks.json (e.g. other preToolUse entries).

Hook paths are relative to ~/.cursor/ (not the project root).

The preToolUse matcher includes SemanticSearch — Cursor agents often use that instead of Grep, and it must be tracked for the remind counter to work.

4. User Rules — Cursor Settings (required)

Global .mdc rules in ~/.cursor/rules/ are not supported. Paste the contents of user-rules.txt into:

Cursor Settings → Rules → User Rules

This is the most important step. Hooks nudge and occasionally deny overuse, but User Rules counteract Cursor's built-in tool bias (the same problem Serena documents for Claude Code and VSCode). Without this paste, agents will keep defaulting to Read / Grep / SemanticSearch on code files.

Optional: keep a local copy for re-paste after Cursor updates:

cp user-rules.txt ~/.cursor/serena-user-rules.txt

Verify

  1. Restart Cursor completely.
  2. Settings → Tools & MCP — Serena shows connected (green).
  3. Settings → Hooks — three Serena hooks listed, no validation errors.
  4. New Agent chat — ask: "What MCP tools do you have?" — Serena tools should appear.
  5. In a repo with .serena/project.yml, the agent should call initial_instructions at session start (injected by the sessionStart hook) and prefer get_symbols_overview / find_symbol over full-file Read for code.

Test hooks from a terminal:

echo '{"session_id":"test"}' | python3 ~/.cursor/hooks/serena_bridge.py activate
# Should print additional_context about activate_project + initial_instructions

How it works

Mechanism Problem solved
Full path in mcp.json MCP subprocess can't find serena on PATH
--context=ide + --project-from-cwd Right tool set; project auto-detected per workspace
sessionStart hook Agent drift — forgets to activate Serena
preToolUse remind hook Agent overuses Read/Grep/SemanticSearch instead of find_symbol / get_symbols_overview
User Rules (pasted in Settings) Built-in tool docs override Serena guidance
sessionEnd cleanup Stale hook counters between sessions

Troubleshooting

Symptom Fix
Agent still uses Read/Grep on code files Paste user-rules.txt into Cursor Settings → Rules → User Rules and restart
Serena tools green in UI but agent can't see them New chat; ask agent to list MCP tools. Try Settings → Network → HTTP Compatibility Mode → HTTP/1.1. Restart Cursor.
serena not found in MCP Use full path in mcp.json: output of which serena (e.g. /home/you/.local/bin/serena)
Hooks don't load Ensure "version": 1 at top of hooks.json. Restart Cursor. Check Settings → Hooks output channel.
Remind hook never fires Ensure matcher includes SemanticSearch if agent uses semantic search; merge hooks don't drop the Serena entry
Serena starts but wrong project Run serena project init in repo root; ensure workspace root contains .serena/project.yml
MCP timeout on slow machines Increase Cursor MCP startup timeout if available

References

Optional: per-project rules

If a repo needs stricter Serena enforcement (e.g. alwaysApply + globs), add .cursor/rules/serena.mdc in that repo only. The global User Rules above are enough for most teams.

{
"version": 1,
"hooks": {
"sessionStart": [
{
"command": "hooks/serena-activate.sh"
}
],
"preToolUse": [
{
"command": "hooks/serena-remind.sh",
"matcher": "Read|Grep|Shell|SemanticSearch"
}
],
"sessionEnd": [
{
"command": "hooks/serena-cleanup.sh"
}
]
}
}
{
"mcpServers": {
"serena": {
"command": "/home/you/.local/bin/serena",
"args": [
"start-mcp-server",
"--context=ide",
"--project-from-cwd"
]
}
}
}
#!/usr/bin/env bash
exec python3 "$(dirname "$0")/serena_bridge.py" activate
#!/usr/bin/env bash
exec python3 "$(dirname "$0")/serena_bridge.py" cleanup
#!/usr/bin/env bash
exec python3 "$(dirname "$0")/serena_bridge.py" remind
#!/usr/bin/env python3
"""Bridge Cursor hooks to serena-hooks (adapted from Serena's Claude Code / VSCode setup)."""
from __future__ import annotations
import json
import subprocess
import sys
from typing import Any
def _read_input() -> dict[str, Any]:
return json.loads(sys.stdin.read())
def _session_id(payload: dict[str, Any]) -> str:
session_id = payload.get("session_id") or payload.get("sessionId")
if session_id:
return str(session_id)
conversation_id = payload.get("conversation_id") or payload.get("conversationId")
if conversation_id:
return str(conversation_id)
raise ValueError("Session ID is required in the hook input data")
def _run_serena_hook(command: str, client: str, payload: dict[str, Any]) -> str | None:
proc = subprocess.run(
["serena-hooks", command, f"--client={client}"],
input=json.dumps(payload),
capture_output=True,
text=True,
check=False,
)
if proc.returncode != 0:
sys.stderr.write(proc.stderr)
return None
output = proc.stdout.strip()
return output or None
def _cursor_session_start_output(serena_output: str) -> None:
data = json.loads(serena_output)
additional_context = data.get("hookSpecificOutput", {}).get("additionalContext", "")
if additional_context:
print(json.dumps({"additional_context": additional_context}))
def _cursor_pre_tool_use_output(serena_output: str) -> None:
hook_output = json.loads(serena_output).get("hookSpecificOutput", {})
decision = hook_output.get("permissionDecision")
if decision != "deny":
return
print(
json.dumps(
{
"permission": "deny",
"agent_message": hook_output.get("additionalContext", ""),
"user_message": hook_output.get("permissionDecisionReason", ""),
}
)
)
def _normalize_pre_tool_use(payload: dict[str, Any]) -> tuple[str, dict[str, Any]]:
"""Map Cursor preToolUse payload to serena-hooks (claude-code or codex) format."""
tool_name = str(payload.get("tool_name") or payload.get("toolName") or "").strip()
tool_input = payload.get("tool_input") or payload.get("toolInput") or {}
lowered = tool_name.lower()
if lowered == "read":
file_path = tool_input.get("path") or tool_input.get("file_path") or tool_input.get("filePath")
return "claude-code", {
"session_id": _session_id(payload),
"tool_name": "read",
"tool_input": {"file_path": file_path},
}
if lowered in {"grep", "rg"}:
return "claude-code", {
"session_id": _session_id(payload),
"tool_name": "grep",
"tool_input": tool_input,
}
if lowered == "semanticsearch":
return "claude-code", {
"session_id": _session_id(payload),
"tool_name": "grep",
"tool_input": tool_input,
}
if lowered == "shell":
command = str(tool_input.get("command") or tool_input.get("cmd") or "").strip()
return "codex", {
"session_id": _session_id(payload),
"tool_name": "shell",
"tool_input": {"cmd": command},
}
return "claude-code", {
"session_id": _session_id(payload),
"tool_name": lowered,
"tool_input": tool_input,
}
def activate() -> None:
payload = _read_input()
serena_payload = {"session_id": _session_id(payload)}
output = _run_serena_hook("activate", "claude-code", serena_payload)
if output:
_cursor_session_start_output(output)
def remind() -> None:
payload = _read_input()
client, serena_payload = _normalize_pre_tool_use(payload)
output = _run_serena_hook("remind", client, serena_payload)
if output:
_cursor_pre_tool_use_output(output)
def cleanup() -> None:
payload = _read_input()
serena_payload = {"session_id": _session_id(payload)}
_run_serena_hook("cleanup", "claude-code", serena_payload)
def main() -> None:
if len(sys.argv) != 2 or sys.argv[1] not in {"activate", "remind", "cleanup"}:
print("Usage: serena_bridge.py {activate|remind|cleanup}", file=sys.stderr)
sys.exit(1)
{"activate": activate, "remind": remind, "cleanup": cleanup}[sys.argv[1]]()
if __name__ == "__main__":
main()
# Serena tool selection (global — paste into Cursor Settings → Rules → User Rules)
When working on code files in a repo that has .serena/project.yml, Serena MCP symbolic tools are PRIMARY. Built-in Read, Grep, SemanticSearch, and StrReplace are SECONDARY.
Built-in tool descriptions assume projects without Serena. When they conflict with this rule, this rule wins. Do not rationalize with "the file is small" or "I already know the path."
Mapping (use Serena, not built-in, for code files):
- File structure overview → mcp_serena_get_symbols_overview
- Read a symbol body → mcp_serena_find_symbol (include_body=true)
- Find symbol by name → mcp_serena_find_symbol
- Find references → mcp_serena_find_referencing_symbols
- Edit a symbol → mcp_serena_replace_symbol_body / insert_*_symbol / replace_content
- Regex search → mcp_serena_search_for_pattern
Before editing code: (1) mcp_serena_initial_instructions once per session if needed, (2) get_symbols_overview on target file, (3) find_symbol for symbols you will touch, (4) edit symbolically.
Built-in Read/Grep are OK for non-code files (markdown, yaml, json, config) or when Serena fails on unparseable/generated code.
At session start in a Serena-enabled repo: call activate_project and initial_instructions if not already done this session.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment