> ## Documentation Index
> Fetch the complete documentation index at: https://mcpjam-mintlify-docs-update-pr-4070-1786998819507.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# MCPJam API

> Programmatic access to the MCP servers in your MCPJam projects — diagnostics, tool calls, prompt rendering, and asynchronous eval runs.

<Warning>
  **The MCPJam API is in preview.** It works today and we use it ourselves, but
  it has not been generally announced and the surface may change — including in
  breaking ways — while we finish the design. Pin your integration to the
  behaviors documented on this page, build a [tolerant
  reader](#versioning--stability), and expect to revisit it. Feedback is very
  welcome on [Discord](https://discord.gg/JEnDtz8X6z) or
  [GitHub](https://github.com/MCPJam/inspector/issues).
</Warning>

<Card title="Create an API key" icon="key" href="https://app.mcpjam.com/settings/api-keys" horizontal>
  Go straight to key management in the hosted app. If you're signed out,
  you'll be asked to sign in and then land right back on the API keys page.
</Card>

The MCPJam API lets you operate the MCP servers saved in your
[hosted](/hosted/overview) projects — from CI, scripts, or your own agents —
without opening the UI.

What you can do with it today:

* **Discover your resources** — list your projects, their servers, eval
  suites, and chat sessions, so every ID the other routes need is
  self-serve
* **Validate a server** — connect, initialize, and capture a capability snapshot
* **Run the doctor** — the probe → connect → initialize → capabilities workflow
* **Check OAuth requirements** — does this server need an OAuth grant?
* **List tools, prompts, and resources** — the server's MCP primitives
* **Call a tool / render a prompt** — execute primitives and get the MCP result back verbatim
* **Read a resource** by URI
* **Export a full snapshot** — tools, resources, and prompts as one JSON document
* **Manage a project's hosts** — list, read, **create** (from a built-in
  template like `claude`/`chatgpt`/`cursor`, or from a full host config),
  rename, and delete the named model + capability profiles you run chats and
  eval suites against
* **Manage a project's environments** — list, read, create, edit, archive, and
  restore the named execution bundles (host + optional server group + optional
  pinned skills and plugin versions) that eval suites and journeys run against,
  and **preview** what one resolves to before launching it
* **List a project's scenarios** — name, access mode, attached servers, and
  share link — and **read one scenario's settings**: model, system prompt,
  tool-approval policy, and resolved servers
* **Author eval suites** — create a runnable suite (name, default model, servers, test cases) synchronously and get a `suiteId` back; no run is started and no credits are spent
* **Run eval suites asynchronously** — create a run from a saved suite, get a `202` + `runId`
  immediately, then poll status, per-iteration results (tool calls, token
  usage, latency), and full traces. Runs appear live in the hosted UI,
  tagged `source: "api"`
* **Import OAuth tokens** — complete OAuth yourself (e.g. the SDK's
  `runOAuthLogin`) and push the tokens; subsequent calls inject and refresh
  them server-side
* **Run a headless agent turn** — send a message history, get back the
  assistant reply, the platform operations it invoked, references to any
  resources it created (eval suites), and token usage; the model is pinned
  server-side and billed to the project

All endpoints operate on servers you have already added to a project in the
hosted inspector. Managing API keys remains UI-only — see
[Not in the API yet](#not-in-the-api-yet).

## Base URL

```text theme={"theme":"css-variables"}
https://app.mcpjam.com/api/v1
```

The API is path-versioned. All routes on this page are relative to the base
URL above.

## Authentication

Every request needs an MCPJam API key in the `Authorization` header:

```bash theme={"theme":"css-variables"}
curl -X POST \
  "https://app.mcpjam.com/api/v1/projects/$PROJECT_ID/servers/$SERVER_ID/tools" \
  -H "Authorization: Bearer $MCPJAM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```

### Creating a key

1. Open [**Settings → API keys**](https://app.mcpjam.com/settings/api-keys)
   in the hosted app (the card at the top of this page takes you there
   directly).
2. Click **Create API key**, name it, and pick the **organization** it will act in.
3. Copy the key (`sk_…`) immediately — **it is shown exactly once** and never
   stored by MCPJam in retrievable form.

Keys can be revoked from the same page at any time. Revocation takes effect
immediately.

### Scope

A key is bound to one MCPJam organization at creation and acts **as you,
inside that organization**. Per-request authorization still applies: a call
only succeeds if the key's owner can access that project and server. A key
can never reach projects outside its organization.

<Note>
  Two deliberate restrictions while in preview:

  * **API keys cannot manage API keys.** Requests to the key-management surface
    with an `sk_…` bearer fail with `403 FORBIDDEN`. Create and revoke keys in
    the UI.
  * **Guest sessions cannot use the API.** Sign in to create keys.
</Note>

### Keep keys secret

Treat an API key like a password:

* Load it from an environment variable or secret manager — never commit it to
  source control or ship it in client-side code.
* Scope keys narrowly: one key per integration, named so you can tell them apart.
* Rotate by creating a replacement key, switching traffic, then revoking the old one.
* If a key leaks, revoke it immediately in
  [**Settings → API keys**](https://app.mcpjam.com/settings/api-keys).

If you see `401 UNAUTHORIZED` with `details.reason: "ORPHANED_KEY"`, the key
is no longer bound to an organization and cannot be used — create a new key
from [Settings](https://app.mcpjam.com/settings/api-keys).

## Conventions

**Requests.** Operations on servers are `POST` with a JSON body (most accept
an empty `{}`). **Reads** — the catalog listings and eval-run polling — are
`GET` and take their options (`cursor`, `limit`, filters) as query
parameters.

**Responses.** Three envelope shapes, used consistently:

| Kind            | Shape                                                                               |
| --------------- | ----------------------------------------------------------------------------------- |
| Single resource | The resource object directly                                                        |
| Collection      | `{ "items": [...], "nextCursor": "..." }` — `nextCursor` omitted on the last page   |
| Error           | `{ "code": "...", "message": "...", "details": {...} }` with a matching HTTP status |

**Pagination.** Collections are cursor-based. Pass the previous response's
`nextCursor` as `cursor` in the next request (body field on `POST` lists,
query parameter on `GET` lists). Cursors are opaque — don't parse them.

**Authoring vs running suites.** `POST /eval-suites` creates a runnable suite
(the suite plus its test cases) **synchronously** and responds `201` with the
`suiteId`, WITHOUT running anything — author once, run later. `POST /eval-runs`
is the async counterpart: it validates and creates a run synchronously, then
detaches execution and responds `202` with a `runId`. Poll
`GET /eval-runs/{runId}` until `status` is `completed`, `failed`, or
`cancelled`.

**IDs.** Every identifier the API takes is discoverable through the API
itself: `GET /projects` lists your projects, `GET /projects/{projectId}/servers`
lists each project's servers, and `GET /projects/{projectId}/eval-suites`
lists its eval suites. Start from `GET /me` to confirm which account a key
acts as.

## Errors

Errors always use the canonical body `{ code, message, details? }`. The `code`
is stable and machine-readable; `message` is human-readable and may change;
`details` is an optional, unstructured bag.

| Code                    | HTTP | Meaning                                                                                                                                                                                                     |
| ----------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `UNAUTHORIZED`          | 401  | Missing, invalid, revoked, or orphaned key                                                                                                                                                                  |
| `OAUTH_REQUIRED`        | 401  | The **target MCP server** needs an OAuth grant — distinct from a bad key                                                                                                                                    |
| `FORBIDDEN`             | 403  | Key is valid but not allowed to do this — or the **target MCP server** rejected the credentials MCPJam presented (retrying with a different MCPJam key will not help; reconnect or re-authorize the server) |
| `VALIDATION_ERROR`      | 400  | Malformed body or parameters                                                                                                                                                                                |
| `NOT_FOUND`             | 404  | Unknown project, server, or resource                                                                                                                                                                        |
| `CONFLICT`              | 409  | The resource isn't in a state that accepts this write — a stale `expectedRevision`, a duplicate name, or an environment that can't currently be launched. Re-read and retry                                 |
| `FEATURE_NOT_SUPPORTED` | 422  | The target server doesn't support this MCP capability                                                                                                                                                       |
| `RATE_LIMITED`          | 429  | Too many requests — see [Rate limits](#rate-limits)                                                                                                                                                         |
| `SERVER_UNREACHABLE`    | 502  | Could not connect to the target MCP server                                                                                                                                                                  |
| `TIMEOUT`               | 504  | The target MCP server connected but didn't respond in time                                                                                                                                                  |
| `INTERNAL_ERROR`        | 500  | Something failed on our side                                                                                                                                                                                |

New error codes may be **added** over time; treat unknown codes as
non-retryable failures unless the HTTP status says otherwise.

## Rate limits

Each key gets **60 requests per minute** sustained, with bursts up to **10**.
Exceeding it returns `429 RATE_LIMITED` with a `Retry-After` header (in
seconds):

```http theme={"theme":"css-variables"}
HTTP/1.1 429 Too Many Requests
Retry-After: 7

{ "code": "RATE_LIMITED", "message": "API key rate limit exceeded. Slow down and retry." }
```

Honor `Retry-After`, add jittered exponential backoff, and expect these limits
to be tuned during the preview.

Other `429`s — the per-minute brakes and daily budgets described with the
endpoints below — carry `Retry-After` whenever the refusal knows when it
lifts, which is the ordinary case. Treat it as a hint that is usually there
rather than a guarantee: a refusal that reaches you without one still needs
your backoff, so **never** block waiting for a header that may not come.

## Endpoints

Each endpoint is fully documented — request and response schemas, examples,
and an interactive playground — under **Endpoints** in the sidebar.

**Catalog** — discover the IDs everything else takes:

| Endpoint                                                      | What it does                                                                                                                                |
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET /me`                                                     | The account behind the key                                                                                                                  |
| `GET /organizations`                                          | The organizations the caller belongs to — where `organizationId` comes from. A key is bound to one organization and only ever sees that one |
| `GET /projects`                                               | Projects the key can access (optionally `?organizationId=`)                                                                                 |
| `GET /projects/{projectId}/servers`                           | The project's saved MCP servers                                                                                                             |
| `GET /projects/{projectId}/eval-suites`                       | The project's eval suites, with latest-run summaries                                                                                        |
| `GET`, `POST /projects/{projectId}/hosts`                     | List hosts, or create one from a template or full config                                                                                    |
| `GET`, `PATCH`, `DELETE /projects/{projectId}/hosts/{hostId}` | Read, rename/edit, or delete a host                                                                                                         |
| `GET /chat-sessions`                                          | Chat sessions (`?projectId=&status=&limit=&before=`)                                                                                        |

**Project environments** — the named execution bundles (one host, optionally a
standalone server group, optionally pinned skills and plugin versions) that eval
suites and journeys run against. Reads need project membership; **every write
needs project admin**. Writes are revisioned: pass the `revision` you last read
back as `expectedRevision`, and a stale value returns `CONFLICT` (409) instead
of overwriting a concurrent edit.

| Endpoint                                                          | What it does                                                                                                          |
| ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `GET`, `POST /projects/{projectId}/environments`                  | List environments (`?includeArchived=true` to include archived ones), or create one                                   |
| `GET`, `PATCH /projects/{projectId}/environments/{environmentId}` | Read one environment, or edit it (send `null` to clear `serverAttachmentId`, `skillSelection`, or `pluginVersionIds`) |
| `GET /projects/{projectId}/environments/{environmentId}/resolve`  | Preview the host config, closed server set, and pinned plugin versions this environment resolves to right now         |
| `POST /projects/{projectId}/environments/{environmentId}/archive` | Archive it — reversible, and frees the name for a new one                                                             |
| `POST /projects/{projectId}/environments/{environmentId}/restore` | Restore an archived environment                                                                                       |

Archive and restore are explicit sub-actions rather than a `DELETE` because the
row is kept and restore has real semantics: it re-checks the name and drops
plugin pins whose version no longer exists.

**Server diagnostics & primitives** (`/projects/{projectId}/servers/{serverId}/...`):

| Endpoint                       | What it does                                                                |
| ------------------------------ | --------------------------------------------------------------------------- |
| `POST .../validate`            | Connect, initialize, and capture a capability snapshot                      |
| `POST .../doctor`              | Full health workflow: probe → connect → initialize → capabilities           |
| `POST .../check-oauth`         | Does this server require an OAuth grant?                                    |
| `POST .../tools`               | List the server's tools                                                     |
| `POST .../tools/call`          | Execute a tool; returns the MCP `CallToolResult` plus additive `durationMs` |
| `POST .../prompts`             | List the server's prompts                                                   |
| `POST .../prompts/get`         | Render a prompt; returns the MCP `GetPromptResult` verbatim                 |
| `POST .../resources`           | List the server's resources                                                 |
| `POST .../resources/read`      | Read one resource by URI                                                    |
| `POST .../export`              | One JSON snapshot of tools, resources, and prompts                          |
| `POST .../oauth/import-tokens` | Store OAuth tokens you obtained yourself; later calls inject + refresh them |

**Eval runs** (`/projects/{projectId}/...`):

| Endpoint                                                   | What it does                                                                                                                                                                                                                                                                                                                     |
| ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `POST .../eval-suites`                                     | Author-only: create a suite + its test cases from `name` + `serverIds` + a default `model` + `tests`, WITHOUT running it; responds `201` + `suiteId`. Each test's case body is an ordered `steps` array (prompt / toolCall / interact / assert); per-test `model`/`provider`/`runs` are optional and fall back to suite defaults |
| `POST .../eval-runs`                                       | Create a run from a `suiteId` (rerun; `serverIds` optional — defaults to the suite's saved servers) or `suiteName` + inline `tests` + `serverIds` (new suite). `environmentId` (requires `suiteId`) runs against one of the suite's attached environments instead. Responds `202` + `runId`                                      |
| `PATCH .../eval-suites/{suiteId}`                          | Edit suite settings, including `environmentIds` — the project environments the suite runs against (non-empty array sets/replaces, `null` clears)                                                                                                                                                                                 |
| `PATCH .../eval-suites/{suiteId}/schedule`                 | Enable/disable scheduled runs; `environmentId` pins which environment they launch                                                                                                                                                                                                                                                |
| `GET .../eval-runs/{runId}`                                | Run status, result, and summary — poll until terminal                                                                                                                                                                                                                                                                            |
| `GET .../eval-runs/{runId}/iterations`                     | Per-iteration results: tool calls, token usage, latency (paginated)                                                                                                                                                                                                                                                              |
| `GET .../eval-runs/{runId}/iterations/{iterationId}/trace` | Full trace: messages + expected-vs-actual analysis                                                                                                                                                                                                                                                                               |
| `GET .../eval-suites/{suiteId}/runs`                       | Recent runs for a suite, newest first                                                                                                                                                                                                                                                                                            |

**Eval result ingestion** (`/projects/{projectId}/eval-ingest/...`) — how `@mcpjam/sdk` saves results from eval runs executed outside the platform (local dev, CI). The `{projectId}` segment accepts the literal `default` for the key org's Default project. Most users never call these directly — set `MCPJAM_API_KEY` and the [SDK reporter](/sdk/concepts/saving-results) does:

| Endpoint                                    | What it does                                                          |
| ------------------------------------------- | --------------------------------------------------------------------- |
| `POST .../eval-ingest/report`               | One-shot: create a run from finished results and finalize it          |
| `POST .../eval-ingest/runs/start`           | Chunked flow: create (or idempotently reuse) a run by `externalRunId` |
| `POST .../eval-ingest/runs/iterations`      | Append an iteration batch to a started run                            |
| `POST .../eval-ingest/runs/finalize`        | Finalize a chunked run (idempotent)                                   |
| `POST .../eval-ingest/artifacts/upload-url` | Mint an upload URL for eval artifacts (JUnit XML, Jest/Vitest JSON)   |

**Agent** (`/projects/{projectId}/agent`) — a headless assistant turn for
building conversational surfaces (the MCPJam Slack app is the first consumer):

| Endpoint         | What it does                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `POST .../agent` | Run ONE assistant turn over a `messages` history (`{role: "user"\|"assistant", content}` items, max 50 messages × 8 KB each, 96 KB total). The model is pinned server-side and billed to the project. The turn can read project servers/suites/runs and **create eval suites**; it can NOT start runs — kick those off yourself with `POST .../eval-runs`. Responds synchronously with `reply`, the `toolCalls` it made, `createdResources` (ids + app links), and token `usage`. Capped at 4 concurrent turns per organization and \~90 s wall clock. **Not idempotent — never blind-retry**: dedupe on your own trigger identity, and on an error response check `details.createdResources` (and, after a lost response, list suites) before retrying, or you'll create duplicates |

**Scenarios** (`/projects/{projectId}/scenarios...`) — read-only:

| Endpoint                         | What it does                                                                            |
| -------------------------------- | --------------------------------------------------------------------------------------- |
| `GET .../scenarios`              | List the project's published scenarios: name, access mode, attached servers, share link |
| `GET .../scenarios/{scenarioId}` | One scenario's settings: model, system prompt, tool-approval policy, resolved servers   |

<Note>
  **Swarms and User Testing are enabled per organization.** Everything in the
  four sections below is behind that gate, enforced server-side. If it is not
  on for your organization, the reads generally return empty and every **write**
  — creating a persona or journey, launching a run, publishing a scenario,
  requesting insights — answers `403` with a message naming the feature, not a
  `404` and not a validation error.

  Five operations are deliberately outside the gate, because every one of them
  *reduces* exposure and spend: cancelling a journey run, unpublishing a
  scenario, and archiving a persona, journey or swarm (each one's `DELETE`). An
  organization that loses access can still stop what is running and clean up
  what it authored.

  Ask us on [Discord](https://discord.gg/JEnDtz8X6z) if you want it turned on.
</Note>

**Swarms — authoring** (`/projects/{projectId}/personas...`, `.../journeys...`,
`.../swarms...`) — the definitions a swarm run executes. A **persona** is a
reusable synthetic character; a **journey** is the task you point one at; a
**swarm** is an authoring container holding defaults for the journeys made
under it. Nothing here starts anything — launching is
`POST .../journeys/{journeyId}/runs`.

| Endpoint                          | What it does                                                                                               |
| --------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `GET .../personas`                | List the project's personas                                                                                |
| `POST .../personas`               | Create a persona (accepts `Idempotency-Key`)                                                               |
| `GET .../personas/{personaId}`    | One persona                                                                                                |
| `PATCH .../personas/{personaId}`  | Update a persona. Journeys reference it by id, so renaming is safe                                         |
| `DELETE .../personas/{personaId}` | Delete a persona. **409** while journeys still reference it                                                |
| `GET .../journeys`                | List the project's journeys                                                                                |
| `POST .../journeys`               | Create a journey (accepts `Idempotency-Key`). `personaId` and `swarmId` must be in this project            |
| `GET .../journeys/{journeyId}`    | One journey                                                                                                |
| `PATCH .../journeys/{journeyId}`  | Update a journey. `null` CLEARS `environmentIds`; `sessionsPerTarget` and `maxTurns` must be sent together |
| `DELETE .../journeys/{journeyId}` | **Archive** a journey — existing runs keep pointing at the definition they executed                        |
| `GET .../swarms`                  | List swarm containers                                                                                      |
| `POST .../swarms`                 | Create a swarm container (accepts `Idempotency-Key`)                                                       |
| `GET .../swarms/{swarmId}`        | One swarm                                                                                                  |
| `PATCH .../swarms/{swarmId}`      | Update the container's defaults. Not a cascade — existing journeys keep their own values                   |
| `DELETE .../swarms/{swarmId}`     | **Archive** a swarm. The journeys under it stay launchable                                                 |

**Swarms — generation** — drafts a model writes for you. Both routes return
candidates and **persist nothing**: feed what you want to keep to the create
routes above. That is also why neither accepts an `Idempotency-Key` — a call
with no effect has nothing to de-duplicate, and offering one would imply the
drafts are stable across retries when they are not.

| Endpoint                     | What it does                                                                                        |
| ---------------------------- | --------------------------------------------------------------------------------------------------- |
| `POST .../personas/generate` | Draft a slate of personas (and journeys for each) grounded in a server attachment or an environment |
| `POST .../journeys/generate` | Draft journeys for a persona passed **by value**, so you can draft before you save                  |

<Warning>
  The generation routes **spend** — they run models on your organization's
  account. They are metered two ways, and the two mean different waits: a
  per-minute burst brake (`429`, retry in seconds) and your plan's daily
  budget (`429`, resets at UTC midnight). Both normally carry `Retry-After` —
  honor it rather than guessing, and fall back to your own backoff on the
  refusal that arrives without one.
</Warning>

**Swarms — runs** (`/projects/{projectId}/journeys/{journeyId}/runs`,
`.../journey-runs/...`) — launching a journey and reading what it produced:

| Endpoint                                | What it does                                                                                |
| --------------------------------------- | ------------------------------------------------------------------------------------------- |
| `GET .../journeys/{journeyId}/runs`     | That journey's runs, newest first                                                           |
| `POST .../journeys/{journeyId}/runs`    | **Launch.** Answers `202` — the fan-out has started, not finished. Send `Idempotency-Key`   |
| `GET .../journey-runs/{runId}`          | The full run: counts, per-target breakdown, per-session `attempts`                          |
| `GET .../journey-runs/{runId}/sessions` | The chat sessions the run produced (summaries, not transcripts)                             |
| `POST .../journey-runs/{runId}/cancel`  | Stop a run. Idempotent — a second cancel is `200` with `alreadyCanceled: true`, not a `409` |

Three things worth knowing before you write the client:

* **Always send `Idempotency-Key` on a launch.** It spends model credits, so a
  retry of a dropped response must not run the journey twice. Replaying a key
  returns the original run with `deduped: true` and starts no second runner —
  and consumes no quota. Omitting the header is read as a request that declined
  to identify itself, and gets a fresh run every time.
* **Check `canceled` before reporting a failure.** A stopped run carries
  `status: "failed"`, because cancellation is recorded as a marker rather than
  a status of its own. `stale: true` is the third case: the runner went silent
  and the watchdog settled the run.
* **`targetId`, not `hostId`, identifies a target.** Two environments can
  resolve to the same host with different servers, so a run can hold two
  targets that share a `hostId`.

<Warning>
  Launching **spends**: a run fans out into `targets × sessionsPerTarget`
  sessions. Two different `429`s can come back — the per-minute burst brake
  (retry in seconds) and your plan's daily launch cap (resets at UTC midnight).
  Both normally carry `Retry-After`; back off on your own when one does not.
</Warning>

**Swarms — insights** — what a run revealed. Read these in order of cost: the
roll-up and the scorecard are deterministic and free, the findings registry
aggregates them, and wave insights run a model.

| Endpoint                                          | What it does                                                                |
| ------------------------------------------------- | --------------------------------------------------------------------------- |
| `GET .../journeys-overview`                       | Project roll-up: recent runs, goal-completion rates, repeat failures, trend |
| `GET .../journey-runs/{runId}/scorecard`          | One run's deterministic rubric result. No model, no spend                   |
| `GET .../journey-findings`                        | Criteria that keep failing, tracked across waves                            |
| `POST .../journey-findings/{findingId}/dismiss`   | Hide a finding                                                              |
| `POST .../journey-findings/{findingId}/undismiss` | Un-hide it                                                                  |
| `GET .../waves/{waveId}/insights`                 | Poll a wave's generated insights. **404 = nobody asked**                    |
| `POST .../waves/{waveId}/insights`                | Request them. `202` — scheduled, not done                                   |
| `DELETE .../waves/{waveId}/insights`              | Cancel a generation still `pending`                                         |

Two arithmetic traps worth naming, because both produce numbers that look
plausible and are wrong:

* **Divide by `sessionsGraded`, never by the session total.** Three failures
  out of four graded sessions, in a run that attempted forty, is 75% — not
  7.5%.
* **`failedGradingCount` is not `failCount`.** A crashed judge is not a
  regression, and folding the two together makes one look like the other.

And one modelling note: a finding's `status` is a lifecycle —
`new | recurring | regressed | resolved`. `dismissed` is *not* one of them.
Dismissal is the orthogonal `dismissedAt`, so a finding can be both recurring
and dismissed, and hiding one never claims it stopped happening.

<Warning>
  Requesting wave insights **spends**, and draws on the `insightsPerDay`
  ledger that is **shared** with eval-run and user-testing insights — burning
  it here takes it from there too. Poll the `GET` rather than re-requesting;
  `force: true` deliberately spends a second time.

  Three refusals, three different meanings: a short `429` is the per-minute
  brake (wait seconds), a `429` naming `insightsPerDay` is the daily ledger
  (its `Retry-After`, when present, counts to UTC midnight), and a `403` means
  the feature is not available to your organization — waiting will never help.
</Warning>

**User testing** (`/projects/{projectId}/environments/{environmentId}/scenario`,
`.../user-testing/scenarios/...`) — publishing an environment for real visitors,
reading what they did, and controlling who can reach it. A **scenario** is a
published environment: one per environment, addressed by its own id once it
exists.

| Endpoint                                                                   | What it does                                                                                                          |
| -------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `PUT .../environments/{environmentId}/scenario`                            | **Publish.** Idempotent — republishing returns the existing scenario, and `created: false` says so. Project **admin** |
| `DELETE .../environments/{environmentId}/scenario`                         | **Unpublish.** The link dies and live sessions end. `deleted: false` when there was nothing published                 |
| `PATCH .../user-testing/scenarios/{scenarioId}`                            | Rename, re-describe, or change `mode`                                                                                 |
| `GET .../user-testing/scenarios/{scenarioId}/sessions`                     | What visitors did: one row per session, with feedback and a first-message preview                                     |
| `GET .../user-testing/scenarios/{scenarioId}/sessions/{sessionId}`         | One session's transcript, paged. Tool payloads and blobs are dropped                                                  |
| `PUT .../user-testing/scenarios/{scenarioId}/guest-execution`              | The spend dial for anonymous visitors: daily credits, computer starts, concurrency                                    |
| `POST .../user-testing/scenarios/{scenarioId}/rotate-link`                 | **Mint a new share link.** No body — the new secret is minted server-side                                             |
| `PUT .../user-testing/scenarios/{scenarioId}/members`                      | Invite someone (or change their role) on an `invited_only` scenario                                                   |
| `DELETE .../user-testing/scenarios/{scenarioId}/members/{memberIdOrEmail}` | Remove someone. Their grant is revoked, ending their live sessions                                                    |
| `POST .../user-testing/scenarios/{scenarioId}/rebind`                      | Point the scenario at a different environment in the same project                                                     |

<Warning>
  **Rotating the link does not evict anyone who already used it.** It stops the
  old URL from granting NEW access; the grants already redeemed under it stay
  valid. That is the single most important thing to know here, because rotation
  is what you reach for when a link leaks — and on its own it does not undo the
  leak. Rotate *and* remove the members you did not intend to have.

  What DOES take effect at once, ending live sessions rather than waiting for
  expiry: unpublishing, tightening `mode`, and removing a member. Those bump
  the scenario's `accessVersion`, and a session minted under an older version
  stops working. None of them reverse by calling the opposite — a re-invited
  member gets a new session, not their old one.

  Read `accessVersion` off the publish and rebind responses. The update and
  rotate responses do not carry it.
</Warning>

**User testing — what visitors did** — the analytics reads, and the model pass
over them. Read these in cost order: metrics, usage and signals are
deterministic and free; the insights request runs a model.

| Endpoint                                                                      | What it does                                                                                               |
| ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `GET .../user-testing/scenarios/{scenarioId}`                                 | The scenario, plus its insights envelope when you may have it                                              |
| `GET .../user-testing/scenarios/{scenarioId}/metrics`                         | Volumes, completion and feedback rates. `?population=real\|synthetic` splits real visitors from swarm runs |
| `GET .../user-testing/scenarios/{scenarioId}/usage`                           | Which models, tools and servers the sessions used. **Check `scan.truncated`** before quoting a rate        |
| `GET .../user-testing/scenarios/{scenarioId}/signals`                         | What the miner found in the current window — and where you get the `windowId`                              |
| `GET .../user-testing/scenarios/{scenarioId}/findings`                        | Every finding raised over this scenario, dismissed ones included                                           |
| `GET .../user-testing/scenarios/{scenarioId}/windows/{windowId}/insights`     | One window's insights. **`404` means nobody asked**, which is not the same as `pending`                    |
| `POST .../user-testing/scenarios/{scenarioId}/insights`                       | **Spends.** `202` with the `windowId` to poll. `409` if the window has not been mined yet                  |
| `DELETE .../user-testing/scenarios/{scenarioId}/insights`                     | Release a generation stuck `pending`. Takes `windowId` in the body                                         |
| `POST .../user-testing/scenarios/{scenarioId}/findings/{findingId}/dismiss`   | Dismiss a finding. Keyed on its stable id, so it stays dismissed when insights regenerate                  |
| `POST .../user-testing/scenarios/{scenarioId}/findings/{findingId}/undismiss` | Undo that                                                                                                  |

The five analytics shapes above are documented as **open objects** on purpose.
Their upstream projections grow with the product and the SDK types them the
same way, so pinning a field list would turn every new metric into a spec
violation. Read what you recognize; ignore the rest.

<Warning>
  Requesting insights **spends**, and draws on the same `insightsPerDay` ledger
  as eval-run and swarm wave insights — one budget, three producers. Poll the
  window read rather than re-requesting; `force: true` deliberately spends a
  second time, which is why it is never inferred from anything else.
</Warning>

**Planning** — one read that answers "may I?" before you try:

| Endpoint                                    | What it does                                                                         |
| ------------------------------------------- | ------------------------------------------------------------------------------------ |
| `GET .../projects/{projectId}/capabilities` | The caller's role, the beta gate's state, plan limits, and the booleans to branch on |

<Note>
  `capabilities` is a **planning aid, not a gate**. Every write still enforces
  independently, so a `true` here that races a flag flip costs you a clean
  `403` rather than an incorrect success — and nothing should consult it
  *instead of* trying. It exists because agent surfaces are static: an MCP tool
  catalog is built with no organization in hand, so the alternative is
  attempting the write and reading the failure, by which point the agent has
  usually already told someone what it was about to do.
</Note>

The same surface is available as an [OpenAPI
specification](https://raw.githubusercontent.com/MCPJam/inspector/main/docs/reference/openapi.json)
for Postman, Swagger UI, or client generation.

## Run evals from the API

The shortest useful loop — create a run with one inline test, then poll:

```bash theme={"theme":"css-variables"}
# 1. Create (responds 202 immediately; -f + jq -e fail fast on errors)
RUN_ID=$(curl -fsS -X POST \
  "https://app.mcpjam.com/api/v1/projects/$PROJECT_ID/eval-runs" \
  -H "Authorization: Bearer $MCPJAM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "suiteName": "smoke",
    "serverIds": ["'$SERVER_ID'"],
    "tests": [{
      "title": "echo works",
      "runs": 1,
      "model": "anthropic/claude-haiku-4.5",
      "provider": "anthropic",
      "steps": [
        { "id": "s1", "kind": "prompt", "prompt": "Use the echo tool to say hi" },
        {
          "id": "s2",
          "kind": "assert",
          "assertion": {
            "type": "toolCalledWith",
            "toolName": "echo",
            "args": { "args": { "text": "hi" } }
          }
        }
      ]
    }]
  }' | jq -er .runId)

# 2. Poll until status is completed | failed | cancelled
curl -s "https://app.mcpjam.com/api/v1/projects/$PROJECT_ID/eval-runs/$RUN_ID" \
  -H "Authorization: Bearer $MCPJAM_API_KEY"

# 3. Inspect per-iteration tool calls, token usage, latency
curl -s "https://app.mcpjam.com/api/v1/projects/$PROJECT_ID/eval-runs/$RUN_ID/iterations" \
  -H "Authorization: Bearer $MCPJAM_API_KEY"
```

`model` takes an id from the hosted catalog — `provider/name` form, e.g.
`anthropic/claude-haiku-4.5` — and runs on your organization's credits.
Provider-native ids (e.g. `claude-sonnet-4-5`) are bring-your-own-key: pass
the key in `modelApiKeys`. A model the API can't execute is rejected at
create time with `VALIDATION_ERROR`; `details.hostedModels` lists the valid
hosted ids for that provider.

Reruns are even shorter: `{ "suiteId": "..." }` reruns the suite exactly as
configured, connecting the suite's saved server selection (the `202` response
lists the resolved `servers`). Pass `serverIds` to override the selection; a
suite with no saved selection requires it (`VALIDATION_ERROR` with
`details.reason: "NO_SAVED_SERVER_SELECTION"` otherwise).

### Running against a project environment

A [project environment](#endpoints) is launchable only through a suite that has
it **attached**. Attach them first with
`PATCH /projects/{projectId}/eval-suites/{suiteId}` and an `environmentIds`
array (send `null` to detach them all; `[]` is rejected — use `null`), then
pass `environmentId` on the run. It must be one of that suite's attached
environments; anything else is a `400` with
`details.reason: "ENVIRONMENT_NOT_ATTACHED"`, raised before any case is
authored or any server connected.

Omitting `environmentId` is meaningful, and depends on the suite:

* **no** attached environments → the run uses the saved server selection, as before;
* **exactly one** attached → that environment is used automatically. The `202`
  response's `environment` field says so;
* **several** attached → `400` with `details.reason: "ENVIRONMENT_REQUIRED"`,
  naming the candidates.

The environment supplies the closed server set — including the servers its
pinned plugin versions contribute — so `serverIds` is not required and is
**rejected** alongside it (`400`), rather than accepted and ignored: honoring
both would connect a different set than the run is stamped with. The same goes
for a `serverIds` override on an environment-based suite
(`details.reason: "ENVIRONMENT_SERVERS_NOT_OVERRIDABLE"`).

The run is pinned to the environment revision resolved at launch: if the
environment changes in between, the run is rejected with `CONFLICT` (409)
rather than executing against a different configuration than the one whose
tools were captured. Use
`GET /projects/{projectId}/environments/{environmentId}/resolve` to see what it
will connect before launching.

Every run records which environment it used. `GET .../eval-runs/{runId}` (and
the run listings) return `environment: { id, name, revision }` — read from the
run's immutable snapshot, not the suite's current attachments — or `null` for a
run that used a saved server selection.

Per-organization
concurrency is capped (default **2** concurrent runs); exceeding it returns
`429` with `details.reason: "CONCURRENT_RUN_LIMIT"` — wait for an active run
to finish.

If a server answers `401 OAUTH_REQUIRED`, complete the OAuth flow yourself
(the SDK's `runOAuthLogin` handles interactive, headless, and
client-credentials flows) and push the result to
`POST .../oauth/import-tokens` once. Subsequent calls inject the stored token
and refresh it server-side.

## Not in the API yet

To set expectations while in preview, these are **not** available over the API
today (most exist in the hosted inspector UI):

* Creating or revoking API keys (UI-only by design — see [Authentication](#authentication))
* Chat and conformance suites
* Browser-based OAuth flows initiated by the API (use
  `oauth/import-tokens` after completing OAuth yourself)

If one of these blocks you, tell us on
[Discord](https://discord.gg/JEnDtz8X6z) — it directly shapes what we
stabilize first.

## Versioning & stability

* The API is **path-versioned** (`/api/v1`). When it reaches general
  availability, breaking changes will require a new version path.
* **During the preview**, breaking changes to v1 may still happen; we'll note
  them in the [changelog](/changelog/overview).
* **Additive changes** — new endpoints, new optional request fields, new
  response fields, new error codes — are considered non-breaking and can ship
  at any time. Write clients that ignore unknown fields.
* Error **codes** are stable identifiers; error **messages** are not.
