# Claude Code, Cursor and MCP

Usero runs a remote [Model Context Protocol](https://modelcontextprotocol.io) server at `https://usero.io/mcp`. Point Claude Code,
Cursor, Claude Desktop, Windsurf or VS Code at it and the agent can read your feedback inbox, pull the clusters with verbatim
quotes, file feedback, and ask Usero to open an AI-written pull request, all from inside your editor.

Nothing to install. One config block with the URL and your API key. Fixes ship on our side. No account yet? The agent can sign you
up itself, see the next section.

## Set up from inside the agent

You do not need a Usero account, an API key or a dashboard visit to start. Add the server without a key and ask the agent to set
you up:

```bash
claude mcp add --transport http usero https://usero.io/mcp
claude -p "set me up with usero, my email is you@example.com"
```

Without a key the server exposes two tools, `start_signup` and `check_signup`, and the agent runs this flow:

1. `start_signup(email, clientName)` sends you an email (subject "Connect Claude Code to Usero", or "Sign in to Usero from Claude
   Code" when the account already exists) and returns a poll token plus `emailSentTo`. The email names the agent that asked
   ("Claude Code", "Cursor", or "an MCP client" for anything we do not recognise) and the IP it came from, and has one button,
   "Connect Claude Code". Any earlier link for the same address stops working the moment a new one is sent.
2. You press the button. The page repeats who asked, from where, and for which account, and has one button, "Yes, connect Claude
   Code", plus "This wasn't me". Opening the page does nothing on its own (link scanners open emailed URLs); pressing the button
   signs you in, creating the account if the email is new. The page never shows a key.
3. The agent polls `check_signup(pollToken)`. After you press the button it receives an API key named after the agent, minted at
   that moment and exactly once. The poll token is dead after that, and both the link and the token expire 15 minutes after
   `start_signup`. No key exists until the agent collects it, so an unpolled confirmation leaves nothing behind.
4. The agent saves the key as `USERO_API_KEY` and adds the server with the `--header` from the next section (for Claude Code,
   `claude mcp remove usero` first if a server by that name is already configured, since Claude Code will not overwrite a server
   name in place) and, now authenticated, calls `create_client(name, repo?)` for your product and `connect_github(clientId)` for
   the GitHub App install URL, then `check_github(clientId)` until the install lands.

An existing account signing in this way gets a new key and nothing else changes. Limits: 5 links per email per hour, 10 per IP per
hour. Every other tool answers 401 until a key is sent.

## Get an API key

1. Sign in and open [your profile](/profile).
2. Under API keys, create a key. Name it after the tool that will use it ("Claude Code on my laptop").
3. Copy it once. Keys look like `usk_live_...` and are only shown at creation.

The key acts as you: the agent sees every client you are a member of and nothing else. Revoke it from the same page.

## Connect your client

Replace `usk_live_...` with your key in each snippet.

### Claude Code

Keep the key in your shell profile and reference it from the config:

```bash
export USERO_API_KEY=usk_live_...   # in ~/.zshrc or ~/.bashrc
claude mcp add --transport http usero https://usero.io/mcp --header 'Authorization: Bearer ${USERO_API_KEY}'
```

The single quotes matter. The shell leaves `${USERO_API_KEY}` alone, `claude mcp add` stores it as written (check with
`claude mcp get usero`), and Claude Code expands it each time it connects. Verified on Claude Code 2.1.261: with the variable set
the server connects, without it the connection fails with a 401 and `claude mcp list` warns "Missing environment variables:
USERO_API_KEY". Anthropic's docs only promise expansion for a committed `.mcp.json`
([environment variable expansion](https://code.claude.com/docs/en/mcp#environment-variable-expansion-in-mcp-json)), so use that
form for a team:

```json
{
	"mcpServers": {
		"usero": {
			"type": "http",
			"url": "https://usero.io/mcp",
			"headers": {
				"Authorization": "Bearer ${USERO_API_KEY}"
			}
		}
	}
}
```

Quick alternative: paste the key in place of the placeholder. It then sits in plain text in `~/.claude.json`.

Add `--scope user` to make it available in every project. Check it with `claude mcp list`, then try
`claude -p "list my usero clients"`.

### Cursor

Create or edit `.cursor/mcp.json` in your project (or `~/.cursor/mcp.json` for every project):

```json
{
	"mcpServers": {
		"usero": {
			"url": "https://usero.io/mcp",
			"headers": {
				"Authorization": "Bearer usk_live_..."
			}
		}
	}
}
```

Open Cursor Settings, then MCP, and confirm "usero" shows its tools.

### Claude Desktop

Settings, then Connectors, then Add custom connector. Enter `https://usero.io/mcp` as the URL. When asked for authentication, add
a header named `Authorization` with the value `Bearer usk_live_...`. If your version of Claude Desktop has no header field, use
the `claude_desktop_config.json` form below with `mcp-remote`:

```json
{
	"mcpServers": {
		"usero": {
			"command": "npx",
			"args": ["-y", "mcp-remote", "https://usero.io/mcp", "--header", "Authorization: Bearer usk_live_..."]
		}
	}
}
```

### Windsurf

Edit `~/.codeium/windsurf/mcp_config.json`:

```json
{
	"mcpServers": {
		"usero": {
			"serverUrl": "https://usero.io/mcp",
			"headers": {
				"Authorization": "Bearer usk_live_..."
			}
		}
	}
}
```

### VS Code (GitHub Copilot agent mode)

Create `.vscode/mcp.json`:

```json
{
	"servers": {
		"usero": {
			"type": "http",
			"url": "https://usero.io/mcp",
			"headers": {
				"Authorization": "Bearer usk_live_..."
			}
		}
	}
}
```

### Anything else

Any client that speaks MCP over streamable HTTP works. The server is stateless JSON-RPC 2.0 over POST, no session id, no SSE. A
raw call looks like this:

```bash
curl -s https://usero.io/mcp \
  -H "Authorization: Bearer usk_live_..." \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

## Tools

Every tool is scoped to the clients your key can access. Read tools are marked read-only so clients that ask before running side
effects can skip the prompt. Argument schemas are strict: an unknown argument is an error, not a silent no-op. Date arguments
accept a full ISO datetime or a bare `YYYY-MM-DD` (read as midnight UTC).

| Tool                  | Arguments                                                                                                                                                                                                  | What it returns                                                                                                                                                                                                                       |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `start_signup`        | `email`, `clientName?`                                                                                                                                                                                     | No key needed. Emails a sign-in link naming the agent and replaces any earlier pending link for that address; returns `pollToken` and `expiresInMinutes`. 5 per email per hour, 10 per IP per hour.                                   |
| `check_signup`        | `pollToken`                                                                                                                                                                                                | No key needed. `pending` until the user confirms on the emailed page, then `ready` with `apiKey` (minted at that moment) exactly once; the token is dead afterwards. Expired tokens return an error naming the fix.                   |
| `create_client`       | `name`, `repo?`                                                                                                                                                                                            | Creates a client (one per product) and returns its `clientId` and dashboard URL. `repo` only works if the GitHub App already reaches it; otherwise leave it out and use `connect_github`.                                             |
| `connect_github`      | `clientId`, `repo?`                                                                                                                                                                                        | Returns the GitHub App install URL bound to the client. Once installed, `repo` picks which of the covered repositories PRs go to (single-repo installs are picked automatically).                                                     |
| `check_github`        | `clientId`                                                                                                                                                                                                 | `pending` until the install lands, then `connected` with the chosen `repo` (or null) and every `repos` the installation covers.                                                                                                       |
| `list_clients`        | `nameContains?`, `limit?` (default 25, max 100), `offset?`                                                                                                                                                 | A page of your clients, newest first, each with id, name, open feedback count, environment names (busiest first) and dashboard URL, plus `totalCount`, `hasMore` and `nextOffset`. Call this first.                                   |
| `search_feedback`     | `clientId`, `environment?`, `query?`, `status?` (open, resolved, all), `source?`, `since?` (created), `resolvedSince?` (resolved), `sort?` (newest, oldest, severity), `hasScreenshot?`, `limit?` (max 50) | Feedback with AI summary, category, severity, verbatim quotes, sender, screenshot count, replay flag, `resolvedAt` and `resolutionMessage`, and dashboard URL. Defaults to open items, newest first. Comment bodies cut at 400 chars. |
| `get_feedback`        | `id`                                                                                                                                                                                                       | One item in full: comment, quotes, person, screenshots, session replay link, clusters, pull requests.                                                                                                                                 |
| `list_clusters`       | `clientId`, `includeAddressed?`, `limit?` (max 100)                                                                                                                                                        | Feedback clusters biggest first with severity, urgency, AI summary, up to three sample verbatim quotes each, and PR state. Clusters span environments.                                                                                |
| `get_cluster`         | `clusterId`, `memberLimit?` (default 50, max 200)                                                                                                                                                          | The cluster with its members (highest confidence first), each with quotes and sender, open and total member counts, plus its pull request if any.                                                                                     |
| `create_feedback`     | `clientId`, `environment?`, `title`, `body`, `pageUrl?`, `userEmail?`                                                                                                                                      | Files a feedback item with source `mcp`. It is classified and clustered like any other item.                                                                                                                                          |
| `get_pr_status`       | `feedbackId`                                                                                                                                                                                               | Whether an AI pull request exists for the item, its status, URL and progress.                                                                                                                                                         |
| `request_ai_pr`       | `feedbackId`, `guidance?`                                                                                                                                                                                  | Usero's agent writes and opens a pull request server-side on the client's connected GitHub repo; the caller needs no local checkout. Returns a `prId`. Capped at 5 per key per day.                                                   |
| `list_forms`          | `clientId`                                                                                                                                                                                                 | Hosted forms and surveys with response counts and public URLs.                                                                                                                                                                        |
| `list_form_responses` | `clientId`, `formId`, `page?`, `limit?` (max 50)                                                                                                                                                           | Responses to one form, newest first, with every answer.                                                                                                                                                                               |
| `get_form`            | `formId`                                                                                                                                                                                                   | One form in full: fields with ids, types and options, settings, published state, response count, public URL, embed URL, builder URL.                                                                                                  |
| `create_form`         | `clientId`, `title`, `description?`, `fields`, `settings?`, `published?`                                                                                                                                   | Creates a form or survey and returns it with its public URL, live immediately. `fields` is a typed list (see below); `settings.surveyMetric` scores it as NPS, CSAT or CES.                                                           |
| `update_form`         | `formId`, `title?`, `description?`, `fields?`, `settings?`, `published?`                                                                                                                                   | Edits a form. Only passed arguments change; `fields` replaces the whole list (keep ids from `get_form`), `settings` merges. `published: false` closes it without losing responses.                                                    |
| `delete_form`         | `formId`                                                                                                                                                                                                   | Deletes a form and every response to it. Irreversible; announced as destructive so clients ask first.                                                                                                                                 |

The `environment` argument takes the environment name you send from the widget. Omit it to search every environment. Use the
literal `no-env` for feedback that was sent without one.

## Build a form from the agent

`create_form` takes the questions as a typed list, so an agent can build a survey from a sentence without reading any source. Each
field is an object with a `type` and the options that type supports:

| `type`                      | Extra arguments                                                  | Answer stored as       |
| --------------------------- | ---------------------------------------------------------------- | ---------------------- |
| `text`, `textarea`, `email` | `placeholder?`                                                   | string                 |
| `number`                    | `placeholder?`, `min?`, `max?`                                   | number                 |
| `select`, `radio`           | `options` (1 to 50 strings)                                      | the chosen option text |
| `multiselect`, `checkbox`   | `options` (1 to 50 strings)                                      | array of option texts  |
| `rating`                    | `max?` (stars, default 5)                                        | integer 1 to `max`     |
| `scale`                     | `min?` (default 1), `max?` (default 5), `minLabel?`, `maxLabel?` | integer `min` to `max` |

Every field also takes `label` (required), `description?` (help text), `required?` (default false), `id?` and `condition?`. Ids
are the keys answers are stored under; omit them and they are generated from the label. A `condition`
(`{ fieldId, operator, value? }`, operators `equals`, `not_equals`, `contains`, `not_empty`) shows the field only when an earlier
field's answer matches.

`settings` is optional: `surveyMetric` (`nps` needs a scale 0 to 10, `csat` a rating, `ces` a scale 1 to 7; turns on the score
panel and mirrors responses into the feedback inbox), `routeResponsesToInbox`, `themeColor`, `successMessage`, `submitButtonText`,
`notifyOnSubmission`.

Ask the agent for "an NPS survey with a follow-up question" and it calls:

```json
{
	"clientId": "client_...",
	"title": "How are we doing?",
	"description": "Two questions, thirty seconds.",
	"fields": [
		{
			"id": "nps-score",
			"type": "scale",
			"label": "How likely are you to recommend us to a friend or colleague?",
			"required": true,
			"min": 0,
			"max": 10,
			"minLabel": "Not likely",
			"maxLabel": "Very likely"
		},
		{
			"id": "nps-reason",
			"type": "textarea",
			"label": "What's the main reason for your score?",
			"placeholder": "A sentence or two"
		}
	],
	"settings": { "surveyMetric": "nps" }
}
```

The result carries `publicUrl` (`https://usero.io/f/<slug>`, live at once), `embedUrl` for an iframe, and `builderUrl` for the
dashboard. To change it later the agent calls `get_form`, edits the field list and sends it back whole with `update_form`, reusing
the ids so responses already collected stay attached to their questions. `update_form` with `published: false` pauses collection;
`delete_form` removes the form and its responses for good. The same field shape is accepted by the
[REST forms API](/docs/forms#api).

## Resources

Two resource templates let an agent pin data into its context without a tool call:

- `usero://clients/{clientId}/clusters`: the open clusters for a client, same JSON as `list_clusters`.
- `usero://feedback/{id}`: one feedback item, same JSON as `get_feedback`.

## Prompts

Three prompt templates. Each takes an optional `clientId`; when omitted and your key has exactly one client, that client is used,
otherwise the prompt tells the agent to pick one via `list_clients`:

- `triage_inbox`: read the open feedback, group it into themes with verbatim quotes, recommend what to fix first.
- `fix_top_complaint`: pick the biggest concrete cluster, find the cause in the current repo, fix it or call `request_ai_pr`.
- `write_changelog_from_feedback`: turn the last two weeks of resolved feedback into a short user-facing changelog entry.

In Claude Code they appear as `/usero:triage_inbox` and so on once the server is added.

## What agents can and cannot do

Can: sign you up and mint their own key; create clients and connect GitHub to them; read every feedback item, cluster, form and
response for your clients; file new feedback; build, edit, close and delete hosted forms and surveys; request an AI pull request
on a client that has GitHub connected.

Cannot: resolve or delete feedback, change client settings, connect other integrations, invite members, touch billing, or see
clients you are not a member of. Those stay in the dashboard on purpose. If your workflow needs one of them from an agent, tell us
through the [contact page](/contact?subject=MCP%20tools).

## Rate limits and caps

| Limit                     | Value                             | What happens past it                                            |
| ------------------------- | --------------------------------- | --------------------------------------------------------------- |
| Tool calls                | 120 per API key per minute        | The tool returns an error naming the seconds to wait.           |
| `start_signup`            | 5 per email and 10 per IP, hourly | The tool returns an error; check the inbox for an earlier link. |
| `request_ai_pr`           | 5 per API key per UTC day         | The tool returns an error; open the PR from the dashboard.      |
| AI pull requests overall  | your plan's monthly allowance     | Same as the dashboard and REST API; the error names the plan.   |
| `search_feedback` results | 50 per call                       | Narrow with `query`, `since`, `resolvedSince` or `environment`. |
| `get_cluster` members     | 50 by default, 200 max            | `membersTruncated` is true and `memberCount` has the full size. |

## Troubleshooting

**HTTP 401 on every call.** The `Authorization` header is missing or the key is wrong. It must read exactly `Bearer usk_live_...`.
Keys are shown once at creation; if you lost it, create a new one on [your profile](/profile), or remove the header and let the
agent call `start_signup` for a fresh one.

**`check_signup` says the token is unknown or expired.** Links and poll tokens last 15 minutes from `start_signup`, and sending a
new link expires the previous one. Ask the agent to call `start_signup` again and open the newest email.

**HTTP 406 from curl.** The transport requires `Accept: application/json, text/event-stream` on every POST (both types, in one
header). Copy the curl example above.

**`list_clients` returns an empty list.** The key is valid but its user is not a member of any client. Sign in to the dashboard
with the same account and check you can see the client there. Keys from a teammate's account see the teammate's clients, not
yours.

**A tool says "Client ... not found".** Either the id is mistyped (ids start with `client_`) or the key's user is not a member of
that client. The same message covers both so ids cannot be probed.

**`request_ai_pr` says GitHub is not connected.** Connect the repo on the client's Integrations page (the error includes the
link), then call again. The PR is opened by the Usero GitHub App, not by your agent's credentials.

**Claude Code shows the server but no tools.** Run `claude mcp list` and check the transport is `http`, not `sse`. Remove and
re-add with `--transport http` if needed.

**Cursor shows a red dot.** Cursor reads `.cursor/mcp.json` on startup. Reload the window after editing it, and make sure the file
is valid JSON (a trailing comma is the usual culprit).

**Responses are slow the first time.** The first call after a quiet period warms the Worker and the database. Later calls in the
same minute are fast.

## Privacy and security

The MCP server is the same Worker and database as the dashboard. Only the SHA-256 hash of your key and its first 12 characters are
stored; each request's key is hashed and matched against that, never written. The server makes no LLM calls of its own: it is a
deterministic interface over your data, and the agent on your side does the thinking.

## FAQ

**Why an API key and not OAuth?** Every client on this page accepts a static header today, and not all of them finish an OAuth
flow, so a key was the shortest path to working everywhere. Keys are created and revoked one at a time on
[your profile](/profile): make one per client, name it after the machine, and revoke exactly that one when the machine goes. Usero
stores the SHA-256 hash of the key plus its first 12 characters for display. On each request the key in the header is hashed and
matched against that hash; the raw key is never written anywhere. The key acts as you, so the agent sees the clients you are a
member of and nothing more. OAuth, with per-client consent and expiring tokens, is the planned next step.

**How much of my context does this use?** Measured against the live server on 2026-09-05 with cl100k tokenisation, so treat the
numbers as approximate: about 300 tokens in Claude Code, about 3,200 in Cursor or Claude Desktop. Claude Code 2.1 defers MCP tool
definitions and loads only the tool names and the server instructions at session start
([tool search](https://code.claude.com/docs/en/mcp#scale-with-mcp-tool-search)), which for the 13 tools an authenticated key sees
is 43 tokens of names plus 252 of instructions. Clients that load every schema up front take the whole `tools/list` response,
2,957 tokens of JSON, plus the instructions.

**What does a real session look like?** A full `claude -p` transcript against the live Usero inbox, every tool call included, is
in [the blog post](/blog/feedback-mcp-server#full-transcript).

## Changelog

| Date       | Change                                                                                                                                                                                                                                                                                                                                                                                                                 |
| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 2026-09-05 | Signup email redesigned around one button; `start_signup` returns `emailSentTo` and `emailSubject`, `check_signup` pending says which inbox to check, and `ready` leads with the `USERO_API_KEY` form of `claude mcp add`, removes an existing `usero` server only when one is configured, and returns `docsUrl`.                                                                                                      |
| 2026-09-05 | v1.3.0: build forms from the agent. `create_form` (typed field list, survey scoring via `settings.surveyMetric`), `get_form`, `update_form` (partial patch, fields replaced whole, settings merged) and `delete_form` (announced destructive). The REST forms API now also accepts `fields` and `settings` as JSON, not only as JSON strings.                                                                          |
| 2026-09-04 | v1.2.1: the emailed link opens a confirm page with a button; opening the page alone no longer signs anyone in, and the key is minted only when `check_signup` collects it. A new `start_signup` replaces any earlier pending link for that address. `list_clients` pages (`limit`, `offset`, `nameContains`) and returns `totalCount` and `hasMore`; per-client `feedbackCount`, `githubRepo` and `createdAt` dropped. |
| 2026-09-04 | v1.2.0: sign up from inside the agent. `start_signup` and `check_signup` work with no API key (emailed confirm page, key minted when the agent collects it, once). `create_client`, `connect_github` and `check_github` finish setup. Server instructions lead with the no-key path.                                                                                                                                   |
| 2026-09-04 | v1.1.0: `create_pr` renamed `request_ai_pr`. `search_feedback` gains `resolvedSince`, `sort`, `hasScreenshot`, `screenshotCount`, `resolvedAt`, `resolutionMessage` and 400-char bodies. `list_clusters` gains `sampleQuotes`. `get_cluster` gains `memberLimit` (default 50). `list_clients` gains `openFeedbackCount` and `dashboardUrl`. Strict argument schemas, server instructions.                              |
| 2026-09-04 | v1.0.0: server launched with 10 tools, 2 resources, 3 prompts. API key auth, 120 calls/min, `create_pr` capped at 5 per key per day.                                                                                                                                                                                                                                                                                   |

Tools are never removed inside 90 days of being announced. A tool on its way out gets "deprecated" in its description first.
`create_pr` was renamed before launch, so no deprecation window applies.
