Getting started
Claude Code, Cursor and MCP
Usero runs a remote Model Context Protocol 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:
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:
start_signup(email, clientName)emails you a sign-in link and returns a poll token. 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. Any earlier link for the same address stops working the moment a new one is sent.- You open the link. The page repeats who asked, from where, and for which account, and has one button, "Yes, connect Claude Code", plus "This was not 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.
- 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 afterstart_signup. No key exists until the agent collects it, so an unpolled confirmation leaves nothing behind. - The agent saves the key into its MCP config (for Claude Code,
claude mcp remove userothenclaude mcp addwith the--headerfrom the next section, since Claude Code will not overwrite a server name in place) and, now authenticated, callscreate_client(name, repo?)for your product andconnect_github(clientId)for the GitHub App install URL, thencheck_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
- Sign in and open your profile.
- Under API keys, create a key. Name it after the tool that will use it ("Claude Code on my laptop").
- 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:
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), so use that
form for a team:
{
"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):
{
"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:
{
"mcpServers": {
"usero": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://usero.io/mcp", "--header", "Authorization: Bearer usk_live_..."]
}
}
}Windsurf
Edit ~/.codeium/windsurf/mcp_config.json:
{
"mcpServers": {
"usero": {
"serverUrl": "https://usero.io/mcp",
"headers": {
"Authorization": "Bearer usk_live_..."
}
}
}
}VS Code (GitHub Copilot agent mode)
Create .vscode/mcp.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:
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:
{
"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.
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 aslist_clusters.usero://feedback/{id}: one feedback item, same JSON asget_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 callrequest_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.
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, 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: 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), 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.
Changelog
| Date | Change |
|---|---|
| 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.