Skip to main content
Glama
udah1

cursor-usage-mcp

README.md
# cursor-usage-optimizer

[![npm version](https://img.shields.io/npm/v/cursor-usage-optimizer?logo=npm&label=npm&color=cb3837)](https://www.npmjs.com/package/cursor-usage-optimizer)
[![GitHub stars](https://img.shields.io/github/stars/udah1/cursor-usage-mcp?logo=github&label=stars)](https://github.com/udah1/cursor-usage-mcp/stargazers)
[![License: MIT](https://img.shields.io/npm/l/cursor-usage-optimizer?label=license&color=blue)](./LICENSE)
[![Built for Cursor](https://img.shields.io/badge/built%20for-Cursor-000000?logo=cursor&logoColor=white)](https://cursor.com)

[![npm downloads](https://img.shields.io/npm/dm/cursor-usage-optimizer?logo=npm&label=downloads&color=cb3837)](https://www.npmjs.com/package/cursor-usage-optimizer)
[![node](https://img.shields.io/node/v/cursor-usage-optimizer?logo=node.js&logoColor=white&color=339933)](https://nodejs.org)
[![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178c6?logo=typescript&logoColor=white)](https://www.typescriptlang.org)
[![Release](https://img.shields.io/github/actions/workflow/status/udah1/cursor-usage-mcp/release.yml?logo=githubactions&logoColor=white&label=publish)](https://github.com/udah1/cursor-usage-mcp/actions/workflows/release.yml)

**Purpose: stop the Cursor agent from burning through your request quota.**

Cursor currently has **two** billing models, and this tool behaves differently on each:

- **Request-based** (legacy / many team plans): a fixed pool of included requests (e.g. **500 /
  month**). Every time the agent stops mid-task to ask you a one-off question, your answer starts a
  **new billable turn** — so a chatty agent quietly eats your quota. That's what `conserve` is for.
- **Usage-based** (current individual Pro / Pro+ / Ultra): no request counter at all. Spend is
  token-priced against two monthly pools — **Cursor Models** (Grok / Composer, generous) and
  **Other Models** (third-party at API rates, e.g. **$20** on Pro). On these plans `conserve` is
  **off**: routing a question through the options UI costs the same tokens as any other turn. What
  actually drives cost is **model choice** and **context size**, not turn count.

This is a **local** MCP server. It reads your **live** usage from the same backend your Cursor
dashboard uses. On a request-based plan it hands the agent a `conserve` flag. When you're consuming
your quota, a bundled rule makes the agent **conserve requests** by:

- **routing questions through Cursor's questions/options UI** (asking is free) instead of open-ended
  "stop and wait" prompts — so it still asks what it needs, without burning a request. Note: only the
  *question* is free; **doing the work still consumes your quota**,
- **batching multiple questions into a single options prompt** (one turn instead of many),
- **only defaulting silently when the choice is trivial** or you didn't answer,
- **cutting needless confirmation round-trips** ("should I continue?").

When your included quota is **used up**, `get_usage` returns `exhausted: true`. On a corporate/team
plan (usage moves to on-demand, covered by the org — not out of your pocket) this is **internal info
for the agent only**: it **silently continues normally** — no approval prompts, no more conserving, and
crucially **no "you're out of requests" reminders or usage/spend numbers volunteered** to you (that's
transparent). The only place usage surfaces is the **verbose footer**, and only when verbose is on.

**Usage cache.** Once exhausted, the decision can't change until the billing cycle resets (used only
goes up), so `get_usage` **serves a cached reading** instead of hitting the network on every task —
turning a ~1s call into a ~10ms read. The cache lives in `~/.cursor-usage/cache.json`, auto-invalidates
at the cycle's `billingCycleEnd` (from the API), and refreshes at most once/day as a safety net. Below
the limit it always fetches fresh (crossing the threshold matters). Verbose isn't affected — the footer
has its own non-blocking background refresher that shares the same cache. Pass `refresh: true` to
`get_usage`, or `login`/`logout`, to force a fresh read.

### How it works (three pieces)

1. **Auth (zero-setup by default)** — `get_usage` reconstructs your dashboard session from the token
   Cursor already stores locally, so on most machines there's **nothing to log in to**. A browser
   `login` flow exists only as a fallback. See [Authentication](#authentication) below.
2. **`get_usage`** — makes a direct authenticated API call (no browser) and returns live usage.
   On a **request-based** plan that's the included-request count (e.g. `278/500`) plus a `conserve`
   decision. On a **usage-based** plan that's token spend and per-pool percentages (Cursor models
   vs API/other), with `conserve: false`.
3. **`conserve-requests` rule** (installed globally) — tells the agent to call `get_usage` at the
   start of each task and follow the conserve behavior above when the flag is on. On usage-based
   plans the flag stays off, so the rule is a no-op.

The threshold controls *when* conserving kicks in: `0` (default) = conserve whenever you still have
requests; `80` = only conserve once you've used 80% of the quota. See
[Tuning](#tuning-when-conserve-mode-kicks-in) below.

> **Heads up / caveats**
> - This calls **undocumented internal Cursor endpoints** (the same ones your dashboard calls).
>   They can change without notice and may be against Cursor's ToS to script against. Personal,
>   read-only use only.
> - The default auth path reads Cursor's local token **read-only** and never stores it. The optional
>   `login` fallback stores a session cookie at `~/.cursor-usage/store.json` (chmod 600, never
>   committed) that expires periodically — re-run `login` when that happens.

## Setup

### Install via npm (recommended)

No clone, no build. Add it to `~/.cursor/mcp.json` and let `npx` fetch it:

```json
{
  "mcpServers": {
    "cursor-usage": {
      "command": "npx",
      "args": ["-y", "cursor-usage-optimizer"],
      "env": {
        "CURSOR_USAGE_THRESHOLD_PCT": "0",
        "CURSOR_USAGE_VERBOSE": "false",
        "CURSOR_USAGE_FOLLOWUP": "false"
      }
    }
  }
}
```

Requires **Node ≥ 22.5** (uses the built-in `node:sqlite`). Reload Cursor — with the default
local-token auth there's **no login step**, just call `get_usage`.

Optionally install the flag-aware self-check hooks (adds `postToolUse` + `sessionStart` entries to `~/.cursor/hooks.json`):

```bash
npx -y -p cursor-usage-optimizer cursor-usage-optimizer-install-hook
```

### From source (contributors)

```bash
git clone https://github.com/udah1/cursor-usage-mcp && cd cursor-usage-mcp
npm install
npx playwright install chromium   # optional: only needed for the browser `login` fallback
npm run build
```

Then point `mcp.json` at the local build (`"command": "node", "args": ["/ABS/PATH/dist/index.js"]`)
and set `CURSOR_USAGE_MCP_DIR` to the repo path so the hook uses your local build. Restart Cursor (or
reload the MCP) after `npm run build`.

## Authentication

By default the server needs **no login**. Cursor keeps its own auth in
`…/Cursor/User/globalStorage/state.vscdb` (a SQLite key/value store) under
`ItemTable → cursorAuth/accessToken` — a JWT whose `sub` claim is your user id. The dashboard's
session cookie has a fixed shape:

```
WorkosCursorSessionToken=<userSub>::<accessToken>
```

where `userSub` is the JWT `sub` with any `auth0|` prefix stripped. `get_usage` reads that file
read-only (via Node's built-in `node:sqlite`), rebuilds the exact cookie the browser would send, and
also picks up your `teamId` from `cursorAuth/cachedTeam`. Nothing is written and the token is never
stored. Because the token rotates, it's re-read on every call.

**Cross-platform.** The `state.vscdb` location is derived from the OS:

| OS | Path |
|----|------|
| macOS | `~/Library/Application Support/Cursor/User/globalStorage/state.vscdb` |
| Windows | `%APPDATA%\Cursor\User\globalStorage\state.vscdb` |
| Linux | `${XDG_CONFIG_HOME:-~/.config}/Cursor/User/globalStorage/state.vscdb` |

Override with `CURSOR_STATE_DB=/abs/path/state.vscdb` for non-standard installs.

**When the local path can't be used** — the server automatically falls back to the browser `login`
flow. This happens if: the MCP runs on a **different machine** than Cursor (no local DB), the file
isn't readable, `node:sqlite` is unavailable (Node < 22.5), or you set `CURSOR_USAGE_FORCE_LOGIN=1`.

Browser `login` fallback (either works):

```bash
# call the `login` tool from inside Cursor (just ask it to "log into cursor-usage")

# — or — standalone CLI flow
npm run login
```

`login` opens a real Chromium window (Playwright), sniffs the network to discover the usage
endpoints, and saves them plus your session cookie to `~/.cursor-usage/`. Run `status` any time to
see which auth source is active (`local-token` vs `login`).

## For the installing agent — do this right after setup

> **Agent instructions.** If you are an AI agent that just installed or set up this MCP for the
> user, run one final step before you finish: **tell the user which flags/modes are available and
> ask whether they want to change any.** Ask through **Cursor's questions/options UI** (the same
> free-to-answer prompt this MCP is built around) — not an open-ended text question. If the MCP is
> already loaded, call `status` first so you can show the user their **current effective values**.

Present these three configurable flags with their current values and let the user pick which (if
any) to change in a single options prompt:

| Flag | Tool | Env override | Default | What it controls |
|------|------|--------------|---------|------------------|
| **Conserve threshold** | `set_threshold` | `CURSOR_USAGE_THRESHOLD_PCT` | `0` (always conserve) | The used-% at which conserve mode turns on. `0` = conserve whenever requests remain; `80` = only near the limit. |
| **Verbose** | `set_verbose` | `CURSOR_USAGE_VERBOSE` | off | Append a `Cursor Usage: X/limit · $used/$limit` footer to every message. |
| **Follow-up** | `set_followup` | `CURSOR_USAGE_FOLLOWUP` | off | End each task with a brief "anything else?" question via the options UI. |

Suggested options to offer: *"Leave defaults"*, *"Raise conserve threshold (e.g. 80%)"*,
*"Enable verbose footer"*, *"Enable follow-up questions"* — plus room for a custom answer. Apply the
chosen changes with the matching `set_*` tool, then confirm the new effective values with `status`.
(Env vars in `mcp.json` override the `set_*` tools — mention this if the user's choice is being
overridden by an env value.)

## Applying changes to existing chats

**New chats** pick up the rule and MCP automatically — nothing to do.

For a chat that was **already open** before you installed/updated this:

1. **Reload the MCP once** (Settings → MCP → toggle `cursor-usage` off/on). This restarts the shared
   server, so every chat — including open ones — sees the latest code and tools on its next turn.
2. **Nudge the existing chat** so it starts behaving immediately (rules are re-read per turn, but an
   explicit nudge guarantees it):

   ```
   From now on follow the conserve-requests rule: call the cursor-usage get_usage tool,
   report my current usage, and if conserve is on — ask via the questions UI (not open prompts),
   batch questions, and only default on trivial choices.
   If verbose is on, end every message with the footer.
   ```

## Version updates (daily check)

The server checks **once a day**, in the background, whether a newer version exists. It auto-detects
how it was installed:

- **npm install** (no `.git`): compares the installed version against the `latest` dist-tag on the
  [npm registry](https://www.npmjs.com/package/cursor-usage-optimizer).
- **git checkout** (`.git` present): compares local `HEAD` against `origin/master` via GitHub's
  compare API (no `git fetch`).

Both are **fully fail-open** (offline / proxy / rate-limit simply surfaces nothing) and run from the
background reminder refresher (plus a non-blocking kick from `get_usage`), so they never add latency.

When an update is available, `get_usage` returns `update.available: true` and the agent asks you
**once**, via the options UI, whether to update. If you **skip**, `dismiss_update` records that version
so you're **not asked again until an even newer version** appears (not daily). If you **accept**, the
agent gives you the right commands for your install (reload the MCP so `npx` fetches `@latest`, or
`git pull && npm run build` for a clone). State lives in `~/.cursor-usage/update.json`. Run
`check_update` any time to check immediately.

## Reminder hooks (flag-aware self-check)

Two optional hooks re-inject a short self-check so agents keep following the rules — because the
always-applied rule alone is a *soft* instruction that fast models often skip or forget mid-chat:

- **`sessionStart`** (`hooks/cursor-usage-optimizer-session-start.sh`) injects the reminder into a
  new conversation's **initial context**, so conserve/follow-up/verbose behavior is in effect from
  the very first turn — before any tool runs. (`beforeSubmitPrompt` can't do this: its output schema
  is `{continue, user_message}` only, with no context-injection field.)
- **`postToolUse`** (`hooks/cursor-usage-optimizer-reminder.sh`) re-injects the reminder as the task
  goes on, **throttled per conversation** (default 120s).

Install both with:

```bash
npx -y -p cursor-usage-optimizer cursor-usage-optimizer-install-hook
```

This copies the scripts to `~/.cursor/hooks/`, adds the `postToolUse` + `sessionStart` entries to
`~/.cursor/hooks.json` (replacing any older cursor-usage entries, preserving other hooks), and cleans
up legacy files. The hooks work for **both** install modes: they prefer a built local clone (default
`~/personal-dev/cursor-usage-mcp`, or `CURSOR_USAGE_MCP_DIR` if set) and otherwise refresh via
`npx -y -p cursor-usage-optimizer cursor-usage-optimizer-reminder`.

> **Why per-conversation throttle?** Earlier versions keyed the 120s throttle by a single **global**
> state file, so with multiple concurrent sessions only one conversation "won" each window and the
> rest were silently skipped — the reminder showed up in some chats but not others. The throttle is
> now keyed by the hook's `conversation_id`, so every conversation gets its own timer.

It's **flag-aware**: `reminder-cli` builds the text from the *current* state, so it only mentions
modes that are actually active — e.g. once the quota is **exhausted** it drops the CONSERVE nudge
(nothing left to conserve), and it omits FOLLOW-UP unless follow-up mode is on. The refresh runs
detached (non-blocking) and shares the usage cache.

Because the hook launches the CLIs **without** the MCP's `env`, the MCP **syncs the effective config**
(env-aware `verbose`/`followup`/`threshold`) into `~/.cursor-usage/store.json` on startup, so the
hook-run CLIs read the same settings you configured via `mcp.json`.

## Tools

| Tool | What it does |
|------|--------------|
| `get_usage` | Reads usage and returns the conserve decision + an **`exhausted`** flag. Call at task start. On request-based plans: included-request count, burn-rate projection. On usage-based plans: token spend + per-pool percentages, `conserve: false`. Always includes on-demand spend, plan, and billing-cycle reset. |
| `usage_breakdown` | This cycle's usage broken down **by model**: cost, which pool it drew from (Cursor models vs API/other), and token totals. Works for individual accounts (`teamId: -1`) as well as teams. Heavier than `get_usage`; call on request. |
| `login` | **Fallback** browser login + endpoint auto-discovery (only needed when the local-token path can't be used). Reports current usage immediately. |
| `logout` | Clears the stored `login` session (cookie + endpoints). Does not affect the local-token path. `forgetBrowser=true` also wipes the saved browser profile. |
| `set_threshold` | Sets the persisted threshold (0-100). Default **0** = conserve whenever requests remain. Overridden by the `CURSOR_USAGE_THRESHOLD_PCT` env var if set. |
| `set_verbose` | Enables/disables the per-message usage footer (persisted). Overridden by the `CURSOR_USAGE_VERBOSE` env var if set. |
| `set_followup` | Enables/disables the end-of-task "anything else?" follow-up question (persisted, default off). Overridden by the `CURSOR_USAGE_FOLLOWUP` env var if set. |
| `check_update` | Forces an immediate check against GitHub for a newer version (bypasses the once/day throttle) and reports how to update. |
| `dismiss_update` | Records that the user declined the current available update, so it isn't surfaced again until a newer version lands. |
| `status` | Shows the active **auth source** (`local-token` vs `login`) and local-token details (state.vscdb path, teamId, token expiry), whether a login session is stored, capture time, and stored/env/effective threshold, verbose, and follow-up settings. |

## Tuning when conserve mode kicks in

The threshold is the minimum **used percentage** at which conserve mode activates, **on
request-based plans only**:

- `0` (default): conserve as long as any requests remain.
- `80`: only conserve once you've used ≥80% of the limit.

On **usage-based** plans (no request quota) `conserve` is always `false`, regardless of the
threshold. Spend is metered in tokens, not turns, so the options-UI trick doesn't save money.

There are two ways to set it, and **the env var wins** if both are set:

**1. Env var (recommended — declarative, in `mcp.json`):**

```json
"cursor-usage": {
  "command": "npx",
  "args": ["-y", "cursor-usage-optimizer"],
  "env": {
    "CURSOR_USAGE_THRESHOLD_PCT": "80"
  }
}
```

(For a local clone, use `"command": "node", "args": ["/ABS/PATH/dist/index.js"]` instead.)

Change the number and reload the MCP. Accepts `0`–`100`. Leave it as `"0"` (or remove it) for the
default always-conserve behavior. If `CURSOR_USAGE_THRESHOLD_PCT` is set, it **overrides** any value
set via the tool below.

## Verbose mode (usage footer on every message)

Set `CURSOR_USAGE_VERBOSE` to `true` in the `mcp.json` `env` to have the agent append a usage footer
to the end of every message:

```json
"env": { "CURSOR_USAGE_VERBOSE": "true" }
```

Footer format (rendered as a fenced code block):

```
Cursor Usage: 290/500 requests · $0.00/$75.00 (as of 16:34)
```

On a usage-based plan (no request quota) the footer reports spend instead:

```
Cursor Usage: $7.61 used (1.6% of included) (as of 16:34)
```

You can also toggle it at runtime without editing `mcp.json` via the **`set_verbose`** tool
(persisted in `~/.cursor-usage`). The `CURSOR_USAGE_VERBOSE` env var, if set, **overrides** the tool
value — remove it from `mcp.json` to control verbose purely via `set_verbose`.

Notes: the numbers reflect the reading from the **start of the task** (not refreshed per message), and
because appending a footer to every message is a model behavior, it may occasionally be missed.
Default is off.

## Follow-up mode (end-of-task "anything else?" question)

Independent of conserve/verbose. When on, `get_usage` tells the agent to **end each task with a brief
follow-up question through Cursor's questions/options UI** — e.g. "Anything else?" with a "No, we're
done" option plus room for an open answer (a more specific question when it fits). Because answering
that UI is free, you almost always get a prompt you can respond to and keep the session going without
spending an extra request to re-engage.

Enable via env in `mcp.json`:

```json
"env": { "CURSOR_USAGE_FOLLOWUP": "true" }
```

Or toggle at runtime with the **`set_followup`** tool (persisted in `~/.cursor-usage`). The
`CURSOR_USAGE_FOLLOWUP` env var, if set, **overrides** the tool value. **Default is off.**

## Threshold, continued

**2. `set_threshold` tool (persisted in `~/.cursor-usage`):**

```
set_threshold { "activationThresholdPct": 80 }
```

Used only when the env var is unset/empty. Run `status` to see `storedThresholdPct`,
`envThresholdPct`, and the resulting `effectiveThresholdPct`.

## How usage is read (no browser at query time)

`get_usage` does **not** open a browser. It makes direct authenticated requests (with the cookie
from the [local token](#authentication), or the stored `login` cookie) to the dashboard endpoints:

- **`/api/usage?user=<sub>`** → the legacy request bucket (`gpt-4.numRequests` / `maxRequestUsage`).
  On request-based plans this is the "X / 500" number. On usage-based plans the bucket is empty
  (`numRequests: 0`, `maxRequestUsage: null`) — there is no request quota.
- **`/api/usage-summary`** → `membershipType`, `isUnlimited`, `limitType`, on-demand spend, and the
  token-priced pools under `individualUsage.plan`.
- **`/api/dashboard/get-aggregated-usage-events`** (`usage_breakdown`) → per-model cost and tokens.
  Individual accounts send `teamId: -1`; team accounts send the real id.
- **`/api/dashboard/teams`** (team accounts) → `requestQuotaPerSeat`.
- **`/api/dashboard/get-hard-limit`** (team accounts) → per-user `$` cap for context.

The browser (Playwright) is used **only during the `login` fallback**.

### Request-based plans

The conserve decision is the **included-request percentage** (the "X / 500" number). For team-billed
accounts the math mirrors Cursor's dashboard: the limit is `500 × requestQuotaPerSeat` and the used
count comes from on-plan spend (`ceil(planUsedCents / 4)`), falling back to the legacy `gpt-4`
bucket when spend is `0` or the seat quota can't be read.

**Budget vs. team cap.** The spend line reports your **actual** on-demand budget from
`individualUsage.onDemand.limit` (authoritative for you), and — for context — the team-wide default
per-user cap from `get-hard-limit` (`hardLimitPerUser`). These usually match, but can differ if your
org sets per-user overrides; the tool flags it when they diverge.

### Usage-based plans (current individual Pro)

There is no request counter. `individualUsage.plan` reports:

| Field | Meaning |
|-------|---------|
| `used` | **Total** model spend this cycle, in **cents**, across both pools |
| `limit` | Third-party / Other Models pool cap in cents (e.g. `2000` = **$20** on Pro) |
| `autoPercentUsed` | Percent of the **Cursor Models** pool (Grok / Composer) |
| `apiPercentUsed` | Percent of the **Other Models** pool |
| `totalPercentUsed` | Blended headline number the dashboard shows ("You've used N% of your included total usage") |

**Do not divide `used` by `limit`.** They describe different pools: `used` is combined spend,
`limit` caps only the third-party pool. Cursor's own `plan.remaining` (`limit - used`) makes the
same mistake, which is why the dashboard shows ~1% while that ratio looks like ~25%. This tool
passes Cursor's percentages through and never reports that mixed-pool remainder.

On these plans `get_usage` sets `planModel: "usage"` and **`conserve: false`**. Cost is driven by
which model you pick (Grok draws from the large Cursor-models pool; Claude/GPT draw from the small
$20 pool) and how much context each turn carries — not by how many times the agent asks a question.

`get_usage` always returns the **raw** JSON per source, so if a field ever looks off you can inspect
`raw` and adjust `parseLegacyBucket` / `parsePlanUsage` / `computeIncludedRequests` / `parseSummary`
in `src/usage.ts`.

## Releasing (maintainers)

Publishing to npm is automated via GitHub Actions (`.github/workflows/release.yml`) and triggered by
a version tag:

```bash
npm version patch      # bumps package.json + creates a vX.Y.Z tag (use minor/major as needed)
git push --follow-tags # pushes the commit and the tag → CI publishes to npm
```

The workflow runs `npm ci && npm run build`, verifies the tag matches `package.json`'s version, then
`npm publish --access public`.

**Auth = npm Trusted Publishing (OIDC) — no token/secret.** The workflow authenticates with GitHub's
OIDC token (`id-token: write`); provenance is generated automatically. One-time setup on npmjs.com:
package → **Settings → Publishing access → Trusted Publisher → GitHub Actions**, and enter:

| Field | Value |
|-------|-------|
| Organization or user | `udah1` |
| Repository | `cursor-usage-mcp` |
| Workflow filename | `release.yml` |
| Environment | *(leave blank)* |

(The workflow upgrades npm to `@latest` in CI because Trusted Publishing needs npm ≥ 11.5.1.)

TDQS

A4.4/5.0

Scored across 10 tools

Disambiguation5/5

Each tool has a clear, distinct purpose: usage reading, auth management, configuration, update handling, and status. Even get_usage and usage_breakdown are well differentiated by weight and intent, with descriptions explicitly stating when to use each.

Naming Consistency4/5

Most tools follow a verb_noun pattern (get_usage, set_threshold, dismiss_update), but a few deviations exist: usage_breakdown has no verb, and login, logout, and status are single words. This is a minor inconsistency that does not impair readability or prediction.

Tool Count5/5

10 tools is well-scoped for a Cursor usage management server. Each tool covers a distinct aspect of the domain without redundancy, and no tool feels superfluous.

Completeness5/5

The tool surface covers the full lifecycle: usage retrieval, detailed breakdown, auth options, configuration settings, update management, and an overall status overview. There are no obvious dead ends or missing operations for the stated purpose.

Maintenance

ActivityMaintained
ResponsivenessNo issues