Extending: MCP, skills, commands & hooks
Everything Cursor has done to budgetcli so far has stayed inside the repo. It read the CSV importer, ran the test suite, edited the money math, learned the project’s conventions from AGENTS.md. But budgetcli keeps some of its truth outside the files. The accounts and transactions it reconciles live in a running Postgres database, not in any config the agent can open. The euro and pound transactions need a live exchange rate the agent has never seen. And the one move you treat as sacred — never let an agent write to the real ledger — is exactly the kind of rule you can’t afford to leave to the model’s good intentions.
Cursor solves each of those with a different extension surface, and this chapter is about which surface a given problem actually calls for — the question that trips people up far more often than the syntax does. There are four, and it’s worth pinning them apart before we touch any one of them:
- Reach — an MCP server is a bridge to a system Cursor otherwise can’t touch: a database, an HTTP API, a SaaS dashboard. You declare it in
mcp.jsonand the model gets new tools it didn’t have. - Structure (procedure) — an Agent Skill is a folder with a
SKILL.mdthat packages a repeatable procedure — the steps you’d otherwise re-explain every session. The agent loads it when the work matches. - Structure (prompt template) — a custom slash command is a saved prompt you fire with
/namein chat. Where a skill is a procedure the agent reasons through, a command is a canned instruction you trigger by hand. - Gate — a Hook is a deterministic subprocess wired to a moment in the agent’s lifecycle. It runs regardless of what the model decided, which is how you enforce a rule the agent can’t talk its way around.
The order of this chapter is the order the need shows up in real work: first widen what the agent can reach, then package the procedures and prompts you keep repeating, then put a wall around the move that must never go wrong.
Reach: MCP servers
Section titled “Reach: MCP servers”You hit the wall the moment you ask Cursor to touch budgetcli’s storage. You ask it to add a column to the reconciliation path, and it writes a query against an accounts table with a balance column — except budgetcli stores money as integer cents in balance_cents, and there’s a currency column the agent never mentioned because it never saw it. The schema lives in a running Postgres database. The agent’s whole world is the files in the repo. So it guesses from the struct definitions, gets the column names half-right, and you spend the turn pasting \d accounts back at it.
That pasting is the gap. The truth isn’t in the codebase — it’s in a live system the agent has no hands on. Cursor’s answer is the Model Context Protocol: you declare the database as an MCP server, and Cursor hands the agent a tool that queries it directly. MCP is configuration, not code — you write JSON pointing at a process or URL that already speaks the protocol; you don’t write a server. One direction only, though: Cursor is an MCP client — it consumes the servers you point it at — and does not itself act as an MCP server you can connect other tools to.
Three transports
Section titled “Three transports”The first decision is how Cursor talks to the server. Cursor supports exactly three transports, under these names — pick by where the server runs and how it’s reached, not by what it does:
- stdio — Cursor spawns and manages a local process and talks to it over standard in/out. The fit when the server is a command on your machine — like a Postgres MCP pointed at your dev database.
- SSE (Server-Sent Events) — local or remote, over HTTP with a streaming response channel.
- Streamable HTTP — local or remote, the current HTTP transport in the MCP spec.
For budgetcli’s local dev database, stdio is the fit: Cursor launches the MCP process itself and pipes to it.
Where mcp.json lives
Section titled “Where mcp.json lives”MCP servers are declared in a file called mcp.json, which Cursor reads from two locations:
- Project —
<repo>/.cursor/mcp.json, committed withbudgetcli, so anyone who clones the repo gets the same database tool. - User —
~/.cursor/mcp.json, which follows you across every project — the right home for a personal tool that isn’tbudgetcli’s business.
The rule mirrors every other Cursor config: a server everyone working on budgetcli should have goes in the project file and gets committed; a server that’s your own habit goes in the user file and stays out of the repo.
The stdio schema
Section titled “The stdio schema”The schema mirrors the common MCP shape. For a stdio (local) server you give it a command, its args, and env (with type set to "stdio", plus an optional envFile):
{ "mcpServers": { "budgetcli-db": { "command": "uvx", "args": ["postgres-mcp", "--access-mode=restricted"], "env": { "DATABASE_URI": "postgresql://localhost:5432/budgetcli_dev" } } }}The command plus args is the argv Cursor spawns, exactly as you’d type it in a shell. The connection string goes in env rather than inline in args, so it stays out of the process listing — and you can resolve it from a real environment variable instead of pasting a live credential into a file you’ll commit.
Once it’s declared, the next time the agent needs the schema it runs the query itself — sees balance_cents and the currency column with its own eyes — and writes the migration against what’s actually there. The schema-guessing turn is gone. And because MCP tools are tools, they sit under the same permission model as everything else: a server that can write to your system is gated exactly like a shell command that can.
Remote servers: url, headers, OAuth
Section titled “Remote servers: url, headers, OAuth”When the system isn’t a local process — budgetcli’s exchange-rate provider is a hosted API — you point at a url instead of spawning a command, and put auth in headers:
{ "mcpServers": { "fx-rates": { "url": "https://rates.example.com/mcp", "headers": { "Authorization": "Bearer ${FX_RATES_TOKEN}" } } }}For a static token, headers is the whole story — resolve it from the environment rather than committing a live credential. For a service that speaks OAuth, Cursor runs the authorization dance for you and holds the tokens, so you never manage a bearer string. Cursor uses a single fixed OAuth redirect URL for all MCP servers:
cursor://anysphere.cursor-mcp/oauth/callbackWhen a provider requires fixed client credentials, you can embed them: mcp.json accepts an optional auth object (CLIENT_ID, CLIENT_SECRET, scopes) alongside url and headers for static OAuth client credentials.
That cursor:// callback is one of a small family of Cursor deep links; another — cursor://anysphere.cursor-deeplink/mcp/install — is the mechanism behind one-click install, the easiest way to add a server you didn’t write yourself.
One-click install: Marketplace and cursor.directory
Section titled “One-click install: Marketplace and cursor.directory”You rarely hand-author mcp.json. Cursor has a curated MCP Marketplace in-app with one-click install, and the community directory at cursor.directory carries an “Add to Cursor” button on each server. That button is an mcp/install deep link — it carries a base64-encoded server config, prompts you to confirm, writes the declaration into mcp.json, and kicks off the OAuth flow if the server needs it. So a remote, OAuth-gated server can go from “found it on the directory” to “authenticated and live” without you touching a config file.
One-click install makes collecting servers easy, and collecting has a price: every enabled server’s tool schemas land in the model’s context at session start, whether or not the session uses them. The database server closes a real gap in budgetcli; a directory-browsing habit stacks up definitions that crowd the window before you’ve asked anything. Mount and unmount below and watch what each server costs:
Structure: Agent Skills
Section titled “Structure: Agent Skills”MCP gave the agent new systems to reach. The next gap is different: there’s a procedure you keep re-explaining. Every time budgetcli gets a new bank’s CSV format, you walk the agent through the same dance — sniff the delimiter, map the columns to the canonical schema, normalize dates to UTC, convert amounts to integer cents, dry-run before writing. The agent can do each step; what it can’t do is remember the recipe across sessions. You’re paying a re-teaching tax on a procedure that never changes.
That’s what an Agent Skill is for. Skills are a first-class Cursor primitive — they shipped in Cursor 2.4, “in the editor and CLI.” A skill is a folder containing a SKILL.md — Markdown plus YAML frontmatter — that packages the procedure once, in a form Cursor loads when the work matches.
Where skills live (and the fallback paths)
Section titled “Where skills live (and the fallback paths)”Cursor discovers skills from project and user locations, and notably reads several fallback directories so a skill written for another tool still works:
- Project —
.cursor/skills/,.agents/skills/(plus legacy.claude/skills/and.codex/skills/). - User —
~/.cursor/skills/,~/.agents/skills/(plus the same legacy~/.claude/skills/and~/.codex/skills/fallbacks).
Those .claude/ and .codex/ fallbacks aren’t an accident: Cursor’s docs state that “Agent Skills is an open standard” and point at agentskills.io. The recognized Claude and Codex directories mean a skill a teammate authored for Claude Code drops into budgetcli and works unchanged — cross-tool compatibility in practice, even though Cursor’s docs don’t formally endorse a named third-party spec.
Author a SKILL.md
Section titled “Author a SKILL.md”The skill is a folder whose name matches the skill’s name, holding a SKILL.md:
budgetcli/└── .cursor/ └── skills/ └── import-bank-csv/ └── SKILL.md---name: import-bank-csvdescription: > Import a bank's CSV statement into budgetcli. Use when the user provides a raw bank export and asks to load, import, or reconcile transactions from it.paths: ["importers/**", "data/statements/**"]---
# Import a bank CSV
1. Sniff the delimiter and header row; do not assume comma-separated.2. Map source columns to the canonical schema: date, description, amount_cents, currency, account_id.3. Parse every date to **UTC** at ingest. Reject ambiguous formats — ask which is day vs month rather than guessing.4. Convert amounts to **integer cents**, never a float.5. Run the importer in `--dry-run` first and show the row count and a three-row sample. Only write after the user confirms.Two frontmatter fields carry the load. name must be lowercase-hyphenated and match the folder. description is the field the agent reads to decide when this skill is relevant — so write it as a trigger condition (“use when the user provides a raw bank export…”), not a title. The body is the procedure itself, in plain imperative steps, exactly the brief you’d give a sharp engineer.
Beyond the required name and description, three optional frontmatter fields are where the control is — paths, disable-model-invocation, and a free-form metadata:
paths— glob scoping. The skill becomes a candidate only when files matching these globs are in play, so an importer skill doesn’t surface while you’re editing the test suite.disable-model-invocation— whentrue, the skill is only included when explicitly invoked via/skill-name; the agent will not automatically apply it based on context. This turns a skill into something that behaves like a slash command — a procedure you trigger by hand, never by the model’s judgment.
Three ways to invoke
Section titled “Three ways to invoke”A skill reaches the agent two documented ways, plus a third that’s often assumed but not confirmed:
- Automatic — the agent reads the
description, decides the current task matches, and loads the skill on its own. This is the default and the point of the whole primitive. /skill-name— you type it in chat to run the skill explicitly, overriding the agent’s judgment. (This is the only way to invoke a skill markeddisable-model-invocation.)
A tidy @skill-name-as-attach syntax — pulling a skill’s content into context without running it — is an appealing symmetry with the rest of @, but it isn’t something Cursor’s skills docs actually describe; they document only the two slash paths above. The / runs / @ attaches split itself — the one you drilled in the daily-edit-loop chapter — is the grammar under all of Cursor’s chat, and it’s worth holding onto as we move to commands; just don’t lean on a skill-specific @ form the docs haven’t confirmed.
Structure: custom slash commands
Section titled “Structure: custom slash commands”A skill is a procedure the agent reasons through. Sometimes what you actually keep retyping is simpler: the same prompt. “Write a conventional-commit message for the staged diff.” “Review this change against budgetcli’s money-handling rules.” Those aren’t multi-step procedures the agent should adapt — they’re a fixed instruction you want to fire verbatim. That’s a custom slash command, introduced in Cursor 1.6.
Commands are stored in .cursor/commands/ as plain Markdown files — one file per command, checked into the repo so the team shares them. The filename is the command name; the body is the prompt:
Write a conventional-commits message for the currently staged diff.
- Use the budgetcli scopes: importer, ledger, reconcile, api.- Subject line under 60 characters, imperative mood.- Body only if the change isn't self-explanatory.- Never invent a scope that isn't in that list.Now typing /commit-msg in Agent chat fires that prompt. It shows up in the / picker alongside your skills and subagents — because in Cursor’s chat, / is the run surface and that picker lists everything runnable.
The argument-passing caveat
Section titled “The argument-passing caveat”Here’s the edge to know before you build a command expecting it to behave like Claude Code’s $ARGUMENTS: Cursor’s argument passing into custom commands is limited, and richer templating — argument support, bash execution, file tagging to match Claude Code — is an open community request rather than a shipped feature.
The practical consequence: write commands that work on the current context — the staged diff, the open file, the selection — rather than commands that expect you to pass typed parameters. A /commit-msg that reads the staged diff is robust; a /rename-symbol <old> <new> that depends on parsing two arguments is fighting the primitive. When you need parameters and branching logic, that’s a sign you wanted a skill (a procedure) or a subagent, not a command (a template).
Team Commands
Section titled “Team Commands”For teams, commands don’t have to live in each repo. Team Commands (shipped in Cursor 2.0, alongside Team Rules) let you define custom commands in the Cursor dashboard and have them automatically applied to all members of your team — the same dashboard-distribution model as Team Rules, with no local file needed. A standard like /commit-msg then applies to every member without anyone committing a .cursor/commands/ file.
A note for anyone who used Notepads
Section titled “A note for anyone who used Notepads”If you used Cursor before late 2025, you reached for Notepads to stash reusable prompts and context. Notepads were deprecated at the end of October 2025. Cursor’s own announcement points to Rules, Commands, Memories, and improved Agent context-discovery as the features that supersede them (Skills hadn’t shipped yet — they arrived later, in 2.4). If you’re carrying old Notepads, the practical migration this chapter would suggest is to split each one by whether it’s a multi-step recipe — now best expressed as a Skill — or a fixed instruction, which becomes a Custom Command.
Gate: Hooks
Section titled “Gate: Hooks”MCP widened what the agent can reach. Skills and commands packaged what you keep repeating. None of them is a guarantee — they all depend, in the end, on the model choosing to do the right thing. There’s one move in budgetcli where “the model usually chooses right” is precisely the wrong assurance: Cursor must never write to your real ledger database. It can read it, reason about it, generate a migration for you to review — but the live table where your actual balances are counted is off-limits to automated writes. An accidental UPDATE there isn’t a bug you catch in review; it’s wrong numbers in your own money.
You could put that in AGENTS.md and ask nicely. But for the one thing that must never slip, you want a wall that fires every single time, no matter what the model decided. That’s a Hook — introduced in Cursor 1.7 to let you “observe, control, and extend the Agent loop using custom scripts”: a subprocess Cursor runs automatically at a fixed point in its lifecycle. The agent doesn’t choose to run it and can’t reason around it.
Where hooks live and the schema
Section titled “Where hooks live and the schema”Hooks are configured in a hooks.json, at the user or project scope (Enterprise installs distribute them centrally from the dashboard):
- User —
~/.cursor/hooks.json - Project —
<repo>/.cursor/hooks.json - Enterprise / Team — platform-managed, dashboard-distributed.
The schema names an event and the command(s) it triggers:
{ "version": 1, "hooks": { "beforeShellExecution": [ { "command": "./.cursor/hooks/protect-ledger.sh" } ] }}The event surface — pick the right moment
Section titled “The event surface — pick the right moment”Hooks attach to lifecycle events, and Cursor’s event surface is the broadest in this course — wider than Claude Code’s. Picking the right event is most of the design. Cursor exposes 21 events across three groups — 18 agent-lifecycle, two Tab, and one app-lifecycle (the list does drift between releases, so re-check it against the hooks docs before you depend on a specific name):
- Agent —
sessionStart,sessionEnd,preToolUse,postToolUse,postToolUseFailure,subagentStart,subagentStop,beforeShellExecution,afterShellExecution,beforeMCPExecution,afterMCPExecution,beforeReadFile,afterFileEdit,beforeSubmitPrompt,preCompact,stop,afterAgentResponse,afterAgentThought. - Tab (inline completion) —
beforeTabFileRead,afterTabFileEdit. - App lifecycle —
workspaceOpen.
That beforeShellExecution, beforeReadFile, beforeMCPExecution trio is the policy-enforcement workhorse — command allowlisting, secret scanning, gating the very MCP servers you wired up earlier — and the read-file and MCP gates have no direct analog in Claude Code or Codex. For our ledger wall we want beforeShellExecution: it fires before a shell command runs, the only moment you can stop a write from ever happening.
Wire it up: stdin in, JSON out
Section titled “Wire it up: stdin in, JSON out”Cursor hands the hook a JSON payload on stdin describing the call, and reads the verdict from the hook’s stdout (JSON) and its exit code:
#!/bin/bash# Field path below is illustrative — verify against the hooks payload schema.input=$(cat)command=$(echo "$input" | jq -r '.command')
# Refuse any write that targets the live ledger database.if echo "$command" | grep -Eqi 'ledger_prod|UPDATE .*ledger|INSERT .*ledger|DELETE .*ledger'; then echo '{"permission":"deny","agent_message":"Writes to the production ledger are blocked. Generate a migration for review instead.","user_message":"Blocked a write to ledger_prod."}' exit 0fi
echo '{"permission":"allow"}'exit 0There are two ways to signal a verdict, and the script above uses the explicit one. The permission field — "allow", "deny", or "ask" — is the nuanced channel: emit it as JSON on stdout with exit 0, optionally with agent_message (fed back to the model as the reason) and user_message (shown to you). The blunt channel is the exit code: exit 2 blocks outright, equivalent to "permission": "deny"; any other non-zero code is fail-open — the action proceeds. That fail-open default is the one to internalize: a crashing hook does not silently block your agent, but it also means a buggy gate can leak. Test the deny path.
Watch the wall hold
Section titled “Watch the wall hold”Cursor finishes some balance-reconciliation work and reaches for the database:
> apply the corrected balances to the ledger
Run psql budgetcli -c "UPDATE ledger_prod SET balance_cents=… "
⊘ Blocked by hook (protect-ledger.sh): Writes to the production ledger are blocked. Generate a migration for review instead.
Understood — I can't write the live ledger directly. I'll emit the corrections as a reviewed migration instead.
Edit migrations/0007_reconcile_balances.sql -- review and apply this yourselfWatch what the agent did with the rejection. It didn’t get stuck — it read the agent_message the hook fed back, understood why, and rerouted to the safe path: a migration you apply by hand. The gate didn’t just stop a bad action; it steered Cursor toward the right one. And it would fire identically with auto-run set to run everything, in a headless CLI run, at 2am with nobody watching. That’s the property no rule, skill, or permission setting can give you: always runs, model-independent.
What you’ve built — and which surface to reach for
Section titled “What you’ve built — and which surface to reach for”budgetcli can now reach past its own files, carry its procedures across sessions, and refuse the one move that must never happen:
- MCP servers gave the agent reach — it queries the real Postgres schema and the live exchange-rate API instead of guessing, declared once in
mcp.json(project to share, user to keep personal), added by hand or in one click from the Marketplace / cursor.directory. - Agent Skills packaged the CSV-import procedure into a
SKILL.mdthe agent loads when the work matches — and, because the format is the agentskills.io open standard, a teammate’s.claude/skills/version works unchanged. - Custom slash commands turned the prompts you kept retyping into
/commit-msg-style templates — the natural home, alongside Skills, for prompts you used to stash in the now-deprecated Notepads. - Hooks put a deterministic wall around the production ledger that fires whether or not the model cooperates.
The distinction worth carrying out of this chapter is the one that’s easy to blur: reach when the agent can’t see the system; structure when you’re repeating yourself; a gate when the model’s judgment isn’t a guarantee you can accept. Reach for the wrong one and you’ll fight it — a hook where you wanted a skill, a command where you needed a subagent. Reach for the right one and the agent stops re-deriving, re-asking, and occasionally getting the unforgivable thing wrong.