> ## Documentation Index
> Fetch the complete documentation index at: https://docs.userkit.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP server

> Connect an AI assistant to your UserKit data with one URL and an API key. Reads everything, writes in nine places, and is scoped to the key's environment.

UserKit speaks [Model Context Protocol](https://modelcontextprotocol.io), so an
assistant that supports MCP — Claude Desktop, Claude Code, Cursor, and anything
else that reads an MCP config — can answer questions about your contacts,
customers, subscriptions and pricing without leaving the tool you were already
in.

There is nothing to install. The server is an endpoint on the API:

```
https://api.userkit.dev/mcp
```

It is authenticated with an ordinary API key (`uk_sk_live_…` /
`uk_sk_test_…`), which means two things worth knowing before you paste one in.

<Note>
  **The key decides the environment, and nothing else can.** No tool takes an
  environment argument. A `uk_sk_test_…` key can only ever show you test data — so
  connecting your test key is the safe way to try this, and it is not a setting
  somebody can talk the model out of.
</Note>

<Warning>
  **An API key is a full-access credential for everything else.** The MCP surface
  reads everything and writes only in the nine places listed below, but the same string works against every write endpoint on `/v1`. Treat the config file you paste it into like any other place a secret
  lives, and use a key minted for this rather than the one your backend runs on —
  keys are free, and a key per purpose is a key you can revoke on its own.
</Warning>

## Connect a client

In the project you want the assistant to work in:

```bash theme={null}
npx userkit-install
```

That writes the config for you — and an `AGENTS.md` carrying the handful of
UserKit rules that compile when broken, picked for what the project turned out
to be. It writes no key: the config names the `USERKIT_API_KEY` variable and
reads it from the environment, so the file is safe to commit. Re-running it is a
no-op, and it owns a delimited block rather than the file, so your own guidelines
and your other MCP servers are left alone.

Pick the clients with `--mcp`: `claude` writes `.mcp.json`, `cursor` writes
`.cursor/mcp.json` and `vscode` writes `.vscode/mcp.json`.

To do it by hand instead, every client stores the same three facts: a name, the
URL, and the header. Two details differ between them, and both fail silently: VS
Code keeps its servers under `servers` where the others use `mcpServers`, and it
expands only its own `${env:NAME}` syntax — a bare `${NAME}` is written through
literally, so the header would carry the characters instead of the key.

<CodeGroup>
  ```json Claude Desktop theme={null}
  {
    "mcpServers": {
      "userkit": {
        "type": "http",
        "url": "https://api.userkit.dev/mcp",
        "headers": {
          "Authorization": "Bearer uk_sk_test_…"
        }
      }
    }
  }
  ```

  ```bash Claude Code theme={null}
  claude mcp add --transport http userkit https://api.userkit.dev/mcp \
    --header "Authorization: Bearer uk_sk_test_…"
  ```

  ```json Cursor theme={null}
  {
    "mcpServers": {
      "userkit": {
        "url": "https://api.userkit.dev/mcp",
        "headers": {
          "Authorization": "Bearer uk_sk_test_…"
        }
      }
    }
  }
  ```

  ```json VS Code theme={null}
  {
    "servers": {
      "userkit": {
        "type": "http",
        "url": "https://api.userkit.dev/mcp",
        "headers": {
          "Authorization": "Bearer ${env:USERKIT_API_KEY}"
        }
      }
    }
  }
  ```
</CodeGroup>

Restart the client, and it will list ninety tools. Ask it something —
*"how many contacts signed up in the test environment?"* — and it will pick one.

## What it can do

| Tool                          | Answers                                                                                                  |
| ----------------------------- | -------------------------------------------------------------------------------------------------------- |
| `run_doctor`                  | What is wired up in this environment and what is not                                                     |
| `search_docs`                 | This documentation, as text rather than links                                                            |
| `list_contacts`               | The contacts in this environment, newest first                                                           |
| `get_contact`                 | One contact, with its identity edges and last survey score — no plan: a subscription belongs to the team |
| `list_segments`               | The audiences defined here, with their rules and member counts                                           |
| `active_contacts_usage`       | Contacts that held a session this month, against the plan's ceiling                                      |
| `list_customers`              | The customers — the teams contacts belong to                                                             |
| `list_subscriptions`          | What your customers are paying you, mirrored from the gateway                                            |
| `get_subscription`            | One subscription with its line items                                                                     |
| `list_invoices`               | Billing history, optionally for one customer                                                             |
| `get_catalogue`               | The plans, prices and feature matrix you sell                                                            |
| `list_help_collections`       | The headings your help centre is filed under                                                             |
| `list_help_articles`          | Every article, drafts included                                                                           |
| `get_help_article`            | One article with its version history and newest text                                                     |
| `get_help_article_version`    | The text of one specific version                                                                         |
| `list_changelog_posts`        | Every release note in this environment, drafts included                                                  |
| `get_changelog_post`          | One post with its full text and how many people opened it                                                |
| `list_funnels`                | The funnels your organization has saved, with their steps and window                                     |
| `get_funnel`                  | One funnel, with its ordered list of steps                                                               |
| `list_checklists`             | Every onboarding checklist in this environment, drafts included, with its steps                          |
| `get_checklist`               | One checklist, with its ordered steps and what each waits on                                             |
| `list_point_rules`            | The point rules of this environment, inactive ones included, with what each has paid                     |
| `get_point_rule`              | One rule: what it waits on, what it pays, its ceiling and its audience                                   |
| `list_point_levels`           | The ladder, with how many people sit in each rung                                                        |
| `list_gamification_metrics`   | The units of your own this environment counts beside points                                              |
| `list_achievements`           | The badges, hidden ones included, with how many people hold each                                         |
| `get_achievement`             | One badge, with its condition and how many hold it                                                       |
| `list_legal_documents`        | The terms your customers agree to, with the version in force                                             |
| `get_legal_document`          | One document with its versions and their state                                                           |
| `get_legal_version`           | The text of one specific version                                                                         |
| `list_message_templates`      | The words your product sends, per channel                                                                |
| `get_message_template`        | One template with its live version and its history                                                       |
| `list_customer_roles`         | The roles people hold inside your customers, with what each grants                                       |
| `list_customer_role_versions` | What a role granted before its last edit                                                                 |

`tools/list` is the contract, and it is answered by the running server — if a
tool takes an argument this page does not mention, the client already knows
about it. Each tool also declares the shape of its answer, so a client can hand
the result to code instead of re-parsing it.

## The nine things it can change

An assistant can write in exactly nine places: your **help centre**, the
**catalogue** you sell, your **terms**, your **changelog**, your **funnels**,
your **onboarding**, your **points**, your **message templates** and the
**roles** inside your customers. Everything else answers questions.

### The help centre

Nine tools: `create_help_collection`, `update_help_collection`,
`delete_help_collection`, `create_help_article`, `save_help_article_version`,
`update_help_article`, `publish_help_article`, `unpublish_help_article` and
`delete_help_article`.

<Note>
  **Writing an article and publishing it are separate calls.** A new or edited
  article is a **draft** — nobody outside your organization can read it until
  `publish_help_article` names a version to put live. So an assistant that has
  written something wrong has not yet said anything wrong to your customers, and
  the review step is a real one rather than a convention.
</Note>

Three things follow from how the help centre is built, and an assistant will
tell you about all three because the tools say so in their answers.

**Every save is a new version, and nothing is ever overwritten.** The history is
the whole record of what an article has said, "restore" is saving an old body
again, and even deleting an article is a different act from taking it off the
site — `unpublish_help_article` keeps every word and `publish_help_article` puts
them back.

**The help centre belongs to the organization, not to an environment.** There is
one of it, shared by live and test — so unlike every other tool here, these
answer `scope: "organization"` and carry no `environment`. There is no second
copy to go and fix.

**Publishing and unpublishing are recorded in your audit trail**, against the API
key that did it rather than against a person. `Settings → Audit` shows the entry
as `API key · <the key's name>`, which is the reason to mint a key for the
assistant instead of lending it one your backend uses: the trail can only be as
specific as the credential is.

### The catalogue

Nine more tools, for the plans, prices and features **you sell** — not the plan
you are on with UserKit: `create_catalogue_plan`, `update_catalogue_plan`,
`create_catalogue_price`, `reprice_catalogue_price`, `archive_catalogue_price`,
`create_catalogue_feature`, `update_catalogue_feature`,
`set_catalogue_plan_feature` and `remove_catalogue_plan_feature`.

<Warning>
  **This is your pricing page.** Unlike the help centre, these tools change what
  real customers are quoted — in the environment your key belongs to, so a test
  key can only ever touch your test catalogue. Ask the assistant to read
  `get_catalogue` and tell you what it is about to do before you let it write.
</Warning>

**A price is never edited, and that is what makes this safe to hand to an
assistant.** A subscription points at the price row and that row is the record of
what somebody agreed to pay, so the database refuses any change to it. Changing
what a plan costs is `reprice_catalogue_price`, which archives the old offer and
creates the new one in a single step — and **existing subscribers are not moved
onto the new amount**. They keep the price they agreed to; moving somebody is a
plan change they consent to at your payment gateway.

**Nothing is deleted.** A plan or a feature is archived, which stops it being
sold and is undone by the same call with `archived: false`. A price is archived,
and putting the offer back means creating a new one. The single exception is
taking a feature out of a plan, which removes that cell of the matrix — putting
it back means stating its terms again.

**Two things are permanent and asking is refused rather than ignored**: a plan's
and a feature's `key` (the string your own code gates on), and how a feature is
measured. A feature measured differently is a different feature.

**What it cannot reach is your payment gateway.** No tool stores, reads or uses
the credential that charges a card — connecting a provider is owner-only and
lives in the panel — and nothing here creates a charge, issues a refund or
touches somebody's subscription.

Every catalogue write lands in your audit trail against the key, exactly as the
help centre's do.

### The terms

Five tools: `create_legal_document`, `update_legal_document`,
`save_legal_version`, `publish_legal_version` and `delete_legal_document`, plus
`list_legal_documents`, `get_legal_document` and `get_legal_version`.

Writing and publishing are separate here too, and the separation matters more
than anywhere else on this surface: a new document and every `save_legal_version`
are **drafts**, and nothing changes for anybody until `publish_legal_version`.

<Warning>
  **A published version is frozen at the database and cannot be edited or
  withdrawn.** Publishing is the act: it can be dated ahead, and who is asked to
  agree again is decided by the highest in-force version that declared a material
  change. `delete_legal_document` works only while nothing was ever published —
  after that, taking a document out of circulation is archiving it.
</Warning>

A document marked `required` is reported as blocking wherever the person's
agreements are read, and nothing more — it never signs anybody out and never
blocks `/v1/contact/token`; what to do about it is your product's decision. Publishing terms must mean "everybody is asked", never
"everybody is locked out".

### The changelog

Four tools: `create_changelog_post`, `update_changelog_post`,
`publish_changelog_post` and `delete_changelog_post`, plus the two reads in the
table above.

**Writing a post and announcing it are separate calls**, as in the help centre —
a new post is a draft nobody outside your organization can see. What is different
here, and it is the sharpest edge on this whole surface:

<Warning>
  **There is no unpublish.** `publish_changelog_post` puts the post on your public
  changelog page — a document a CDN may keep — makes the widget's unread badge
  count it, and delivers `changelog_post.published` to your webhooks. The only way
  back is `delete_changelog_post`, which recalls none of that and takes everybody's
  read state with it. An assistant should publish because you asked for that
  release to be announced, never to tidy up a list of drafts.
</Warning>

**A targeted post is not on the public page.** `segment_id` (or
`feedback_post_id`, for the people who voted for a request) narrows the audience,
and a narrowed post is reachable only by somebody signed in to your product —
the public document has no reader to resolve, and answering one per caller-named
identity would turn it into an oracle for who is in which segment. So an
assistant that aims a post at a segment and then checks the public page will
correctly see nothing there.

`list_segments` is where those ids come from, and it carries each segment's rule
as well as its name — so an assistant picks an audience by what it means rather
than by what it is called.

Everything else on this surface still cannot change anything — see below.

### The funnels

Three tools: `create_funnel`, `update_funnel` and `delete_funnel`, plus the two
reads in the table above.

A funnel is a **saved question** — a name, an ordered list of event names and a
window in days — rather than a measurement. It stores no number at all: the
drop-off from one step to the next is recomputed out of the raw events every
time somebody opens the chart. That is why this is the mildest of the four
exceptions: a wrong `delete_funnel` destroys the question, which you state
again, and nothing anybody measured.

Two things are worth knowing before you ask for an edit.

<Note>
  **A funnel belongs to the organization, not to an environment.** There is one of
  it, live and test share the definition, and only the numbers are per
  environment. A test key editing a funnel is editing the same funnel live reads —
  which is the opposite of the changelog, where a post belongs to one environment
  and the other's key cannot reach it.
</Note>

**Changing the steps rewrites the past.** With no stored history to disagree
with the new definition, reordering the steps or swapping an event re-answers
the earlier months too: a chart somebody screenshotted last week will not
reproduce. `update_funnel` replaces the step list **wholesale** rather than
patching it, so read the funnel first with `get_funnel`.

The numbers themselves are not here: they need a range and an environment, and
they are the panel's chart. No tool on this surface computes one.

### The onboarding

Six tools: `create_checklist`, `update_checklist`, `delete_checklist`,
`add_checklist_step`, `update_checklist_step` and `delete_checklist_step`, plus
the two reads above.

This is the exception whose failure is quietest, and the tools are deliberately
narrower than the panel because of it. A changelog post written wrong is wrong on
a page somebody reads; a **step** written wrong produces nothing at all — an
event type nobody publishes is a step that never goes green, and you find out
weeks later as a completion rate you cannot explain.

<Note>
  **Nothing here turns a checklist on.** `create_checklist` makes one that is off,
  and no tool takes `active`. Activating is the moment measurement starts for the
  whole audience at once, and it stays a thing a person does in the panel after
  looking at the steps.
</Note>

An `event` step names one of **our** facts and a wrong name is refused. A
`product_event` step names one of **yours** — whatever your product sends through
`track()` — and that vocabulary is open, so nothing can refuse a name for not
existing. `list_checklists` carries `sightings` for those: a zero means this
environment has never received that event, which is almost always a typo.

**Steps are added and removed, never repointed.** What satisfies a step — the
kind, the event type, the feature key — is not editable here or in the panel:
people already hold progress against it, and changing what it means would leave
rows saying somebody did something they were never asked to do. A step waiting on
the wrong fact is deleted and added again, which loses exactly the progress it
should.

**Nothing satisfies a step, either.** No tool on this surface can mark anybody's
onboarding done, because none exists anywhere: a step is satisfied by a fact
landing on the bus or by an entitlement being held.

`update_checklist` also carries the guide's shape — `unlocks_after` puts one
checklist behind another, and `selectable` makes one a choice the person makes
for themselves. Two checklists waiting on each other are refused: neither would
ever open.

`snooze_days` is how long a close holds for: seven by default, `0` for the
checklist that really is a one-time offer. Somebody dismissing a setup guide
usually means "not now", so the card comes back — and changing the number never
moves a snooze already running.

Both the checklist and the step take `hint_pending` and `hint_done`: what the
line says on hover before and after, because the same line means different
things in the two states. A checklist takes a third, `hint_locked` — a padlock
tells the reader which door, not why it is there. A step has no locked state, so
it has no third.

A step also takes `action_url`, a path in the tenant's app or an absolute
`http(s)` URL, which makes the whole row a link. Setting one on almost every
step is the cheapest improvement available to a guide: a checklist that names
the work and hides the door makes the reader navigate twice.

### Points

Twelve tools: `create_point_rule`, `update_point_rule`, `delete_point_rule`,
`create_point_level`, `update_point_level`, `delete_point_level`,
`create_achievement`, `update_achievement`, `delete_achievement`,
`create_gamification_metric`, `update_gamification_metric` and
`delete_gamification_metric`, plus the six reads in the table above.

A **metric** is a unit of your own beside the points balance — courses finished,
emails sent — credited by the same rules through the same ledger. Creating one
changes nothing on its own: a metric no rule credits stays at zero for everybody,
so the order that works is metric, then rule, then level.

<Warning>
  **None of them gives points to anybody.** Granting is `POST /v1/points`, a
  deliberate act by your backend with a required `request_key`. No tool here takes
  `contact_id`, `external_id` or `amount` — configuring what a product rewards is a
  draft somebody reviews; paying somebody is not.
</Warning>

`award_limit` is required and refused at the tool rather than only at the
database. That is what separates this exception from the other eight: "reward
people for using the app" is a sentence that paraphrases straight into a rule
pointed at the busiest event your product sends, and a rule with no ceiling
writes a ledger line per occurrence, forever.

**A rule and a badge are created OFF** — but unlike a checklist,
`update_point_rule` and `update_achievement` can turn them on. The asymmetry is
retroactivity: turning a checklist on starts measuring an audience against steps
some of them may already have satisfied, while a rule pays nobody retroactively —
switching one on begins counting facts that have not happened yet, and switching
it off costs nothing already earned. A level has no switch at all: it does not
fire, it is what a balance *means*.

**What a rule listens to and what a badge requires do not change.** `key`, `kind`
and `event_type` on one; `kind`, `rule_id`, `threshold` and `segment_id` on the
other — refused with a sentence, never silently ignored. The lines already
written say "you earned this for X", and the badge on somebody's shelf says "you
did this". Re-pricing is allowed and is never retroactive.

The three deletes differ from each other, and each answer says which is which.
Deleting a **rule** keeps the ledger — every line carries the name the rule had at
the time — and the answer carries `ledger_kept`; it is refused while a badge
counts that rule, and the error says how many. Deleting a **level** only loses the
record of who was congratulated for reaching it. Deleting a **badge** is the one
that takes something from people: the unlocks go with it, and the answer carries
`unlocks_removed` with the count — read before the delete, because afterwards
there is nothing left to count.

### Message templates

Six tools: `list_message_templates`, `get_message_template`,
`create_message_template`, `update_message_template`,
`restore_message_template_version` and `archive_message_template`.

These are the words your product sends — the text a customer reads. They are
only here because **an edit became a version**: what was live stays on the
shelf, and `restore_message_template_version` puts it back. An unfortunate
paraphrase is one restore away rather than a sentence you lost.

Writing a template sends nothing. A notification goes out when your backend
posts to `/v1/notifications` naming the `key`; a campaign goes out when somebody
arms it. No tool here does both, and that is the door kept shut: an assistant
that could write the words and mail them would put a paraphrase in front of an
audience.

`update_message_template` replaces the body **wholesale**, so read it with
`get_message_template` first — an edit made without reading drops whatever it
did not know about. Omitted fields carry the live version's value forward, so
changing only the subject keeps the body.

There is no delete. A template names what people were sent, so leaving the
picker is `archive_message_template` and the row keeps resolving the history.
And the platform's own templates — the dunning sequence — are refused: that
sequence cannot be paused, and its wording is not yours to rewrite from here.

### The roles inside your customers

Six tools: `list_customer_roles`, `list_customer_role_versions`,
`create_customer_role`, `update_customer_role`,
`restore_customer_role_version` and `delete_customer_role`.

These are the roles the people on your customers' teams hold, per environment.
`owner` and `member` are seeded into every environment and cannot be deleted or
renamed; everything beside them is your own vocabulary.

A role's permission list holds two of those vocabularies. A permission beginning
with `$` must be one of the four reserved capabilities UserKit evaluates —
`$team.manage`, `$billing.manage`, `$keys.manage` and `$webhooks.manage` — and
anything else in that namespace is refused rather than stored. Everything unprefixed is yours: stored,
carried in the contact's JWT, and never read by us.

<Note>
  **An edit is a new version, never an overwrite**, which is what makes this
  admissible here at all. `update_customer_role` replaces the whole permission set
  and moves the role's head to a new immutable version, so the set you replaced is
  still in `list_customer_role_versions` and `restore_customer_role_version` puts
  it back with one call.
</Note>

Two edits are refused with a sentence. The `owner` role may not give up
`$team.manage` — the guard that keeps a team administrable runs when a membership
moves, so stripping it here would leave every team in the environment with nobody
who can administer it. And a role somebody currently holds cannot be deleted.

`key` is immutable for the reason a plan's key is: it is the string your own code
compares and the one the JWT carries, so renaming it would break your gate with
no error raised anywhere. Renaming for display is `name`, which changes freely.

## Prompts, which are the work rather than the data

The server also serves **prompts**: the jobs people actually bring to this API,
written down once. Clients that support them render them as something you pick —
Claude Code shows them as slash commands.

| Prompt                  | What it sets the assistant to do                                                                               |
| ----------------------- | -------------------------------------------------------------------------------------------------------------- |
| `integrate_userkit`     | Wire UserKit into the project in the current directory, in the arrangement that fits it, and verify the result |
| `diagnose_installation` | Run the doctor and explain what is broken, with the evidence and the repair                                    |
| `plan_go_live`          | Work out what is left before this project serves real customers                                                |

They are rendered per call rather than being fixed text, which is the reason
they live on the server instead of in a plugin you install. `integrate_userkit`
already knows which keys the environment holds and whether its identity secret
has been read, so the assistant is told to wire a signed boot rather than to
invent a sign-in page this API does not answer. `plan_go_live` says
out loud whether the key you connected can actually see the environment you are
asking about.

None of them carries a key, for the reason in the section below.

## What it deliberately cannot do

**It cannot change anything outside the nine places above.** No tool creates, updates, deletes or merges a contact, a customer or
a subscription, and none changes a configuration. That is not a phase of the work: the caller is a
model acting on a sentence somebody typed, and "tidy up the duplicate contacts"
is one paraphrase away from a merge that cannot be undone. The acts are all still
there on the regular API, where a person wrote the call.

Every exception is admitted by what a wrong call can destroy, one at a time. The
help centre touches prose and a pointer: every edit is a new version rather than
an overwrite, and the worst outcome reachable is text on a help page. The
catalogue describes what you sell without being able to rewrite it: prices are
immutable, plans and features archive rather than delete, and what a customer
agreed to pay is out of reach by construction. The changelog is prose again, with
one act that has no opposite — which is why publishing says so in the tool itself
and why creating always writes a draft. A funnel is a saved question about data it
cannot touch: deleting one costs the definition, never the measurement. The
message templates and the customer roles are here for the same reason as each
other: an edit writes a new immutable version and moves a pointer, so what was
live is one restore away rather than a thing you lost. The terms are the
strictest of the nine, and it is publishing that carries the weight — writing is
always a draft, a published version can never be edited or withdrawn, and the
tool says so before it acts.

All of them pass the same permission test, which is what keeps this honest: minting
an API key needs owner or admin, and every one of these writes was already
within reach of those roles. A tool that gave a key-holder something a session
would not does not belong here — which is exactly why the audit trail, which is
owner-only, stays out.

**It sends nothing to anybody's inbox.** Publishing a release note does not mail
your users: the changelog page and the widget's badge are things they come and
read. No tool on this surface sends email.

**It cannot move money.** No charge, no refund, no plan change on somebody's
subscription, and no access to your payment gateway's credentials — connecting a
provider is owner-only and stays in the panel, where a person pasted the key.

**It cannot read your staff audit log.** `audit:read` is owner-only — the record
of what an administrator did is not the administrator's to curate — and an API
key carries no role at all. Serving the audit trail here would route owner-only
records out through a credential anyone who can mint keys already holds.

**It cannot answer with a credential.** No secret keys, no publishable keys, no
sessions, no one-time tokens. A conversation with an assistant is stored,
summarised and rendered wherever the client runs, which makes it the last place a
credential should be printed.

## Reading the answers

Three things surprise people, and all three are deliberate.

`search_docs` **answers sections, not pages, and answers an empty list when it
finds nothing.** That empty list is a real answer — it means this documentation
does not cover the question, which is worth hearing rather than having filled in
from an assistant's memory of some other product's auth API. It searches one
language tree at a time (`en` by default, `pt-br` on request), and the index it
reads ships with the deployment, so what it says is current for the API you are
talking to.

`run_doctor` **has a third status, and it is not a pass.** Every check answers
`ok`, `attention` or `unknown`, and `unknown` means the check could not tell —
the key set is served from a CDN, so a fetch a cache answered never reached us,
and "never seen by us" is not "never fetched". A check that does not apply to
this environment is left out of the list rather than reported as `ok`. Assistants
summarise, and the summary to watch for is the one that turns four `ok`s and two
`unknown`s into "everything is working".

`active_contacts_usage` **answers zero in a test environment**, with
`counted: false` beside it. Test contacts are never metered, so that zero is a
statement about billing rather than about your users. Point the assistant at a
live key when the question is "how many active users do we have".

**Amounts are minor units.** A price carries `amount_minor` and
`currency_exponent`, so `1000` is R\$10.00 and also ¥1000. The assistant has both
numbers; ask it to format, not to divide by a hundred.

## Checking the connection without a client

The [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) talks
to the endpoint directly, so it answers "is it me or is it them" before you go
looking through an editor's logs:

```bash theme={null}
npx @modelcontextprotocol/inspector
```

Connect it with transport **Streamable HTTP**, the URL
`https://api.userkit.dev/mcp`, and an `Authorization` header of
`Bearer uk_sk_test_…`. If the tools list there and not in your editor, the
problem is the editor's config file; if they list in neither, it is the key.

## Troubleshooting

**The client connects and lists no tools.** Almost always the header. The
endpoint answers `401` with the usual error envelope when the credential is
missing or is not a `uk_sk_…` key — a publishable key or a panel session is
refused here, and the message says which one you sent.

**Everything is empty.** Check which key you pasted. An empty answer from a test
key is the environment boundary doing its job, not a broken read; the tools
report the environment they answered for in every response.

**The assistant says it hit a rate limit.** The MCP endpoint carries the same
per-key limit as the rest of the API — 1000 requests a minute per key, with a
3000-a-minute ceiling across an environment. An agent walking a large list in a
loop is exactly the caller that finds it. Ask for a narrower question, or give
the agent its own key so a long crawl cannot spend your backend's allowance.
