# MCP

> Connect an MCP client to the Probara MCP server.

Probara exposes a **Model Context Protocol (MCP)** server at:

```
https://app.probara.net/mcp
```

Any MCP-compatible client (an AI assistant or IDE) can connect to this endpoint to call Probara's test-management tools — projects, suites, test cases, runs, defects, and more. See the [MCP tool reference](https://docs.probara.net/en/developers/mcp-tools/) for the full, generated list of every available tool.

## Authentication

`/mcp` accepts either credential type as a Bearer token — see [API authentication](https://docs.probara.net/en/developers/authentication/) for the full comparison:

- **OAuth 2.1** (recommended for interactive clients like Claude.ai, Claude Code, Cursor, ChatGPT) — the client discovers the server, registers itself, and a human approves access via a consent screen. No secret to copy/paste.
- **API token** (recommended for scripts and headless integrations) — a long-lived `probara_...` token created in **Avatar menu → API tokens** (`/user/api-tokens`), pasted directly into the client's Bearer/Authorization field.

An [app token](https://docs.probara.net/en/developers/authentication/#app-tokens) (`probara_app_...`, from **Workspace → Integrations**) is not one of them: it only reports results from CI, and `/mcp` answers it `403 forbidden`.

A `/mcp` request without a credential returns `401` with a `WWW-Authenticate: Bearer resource_metadata="https://app.probara.net/.well-known/oauth-protected-resource"` header pointing the client at discovery.

A credential the server does not accept (unknown, revoked, expired, or issued for another resource) also returns `401`, with `error="invalid_token"` in that header. An OAuth client then refreshes its token or authorizes again.

## Claude.ai (custom connector)

1. Open **Settings → Connectors → Add custom connector**.
2. Enter the server URL: `https://app.probara.net/mcp`.
3. Claude.ai discovers the OAuth metadata automatically, registers itself (Dynamic Client Registration), and redirects you to Probara's consent screen.
4. Pick the organization to bind the connection to and approve. Claude.ai now has an OAuth grant scoped to that organization.

## Claude Code

1. Add the server: `claude mcp add --transport http probara https://app.probara.net/mcp`.
2. Claude Code opens a browser window for the OAuth authorize/consent flow (same discovery + DCR as Claude.ai).
3. Approve the negotiated scopes and pick an organization. If the client omits `scope` or sends a value Probara doesn't recognize, it defaults to the full supported set (`mcp:read mcp:write`); see [Scopes](https://docs.probara.net/en/developers/authentication/#scopes) for the exact negotiation rules.
4. Verify the connection: `claude mcp list` should show `probara` as connected.

## Cursor

1. Open **Settings → MCP → Add new MCP server**.
2. Choose **HTTP** transport and set the URL to `https://app.probara.net/mcp`.
3. Cursor follows the same OAuth discovery/DCR/consent flow; alternatively, paste an API token into the connector's Bearer/Authorization header field if your Cursor version does not yet support MCP OAuth.

## ChatGPT

1. Open **Settings → Connectors → Add MCP server** (Developer Mode / custom connectors).
2. Set the server URL to `https://app.probara.net/mcp`.
3. Complete the OAuth consent flow, or supply an API token as a Bearer header if your ChatGPT plan only supports static-header connectors.

## Protocol

The endpoint serves two MCP protocol revisions. The revision a client negotiates decides the shape of its answers. Every request is a `POST`: the server keeps no standalone stream, so `GET` and `DELETE` on `/mcp` answer `405`.

- **2025 revisions**, such as `2025-11-25`, are served statelessly, and each answer is framed as Server-Sent Events (`text/event-stream`).
- **`2026-07-28`** clients get one JSON body per request and discover the server with `server/discover`. Each of their requests carries the `MCP-Protocol-Version` and `Mcp-Method` headers, and `tools/call`, `prompts/get` and `resources/read` requests also carry `Mcp-Name` with the tool's name, the prompt's name or the resource's URI. The headers must match the body. A request with a header missing or disagreeing is refused with HTTP `400` and JSON-RPC error `-32020`.

The answers to `tools/list`, `prompts/list`, `resources/list`, `resources/templates/list` and `server/discover` for `2026-07-28` clients carry `ttlMs: 300000` and `cacheScope: "private"`. A client may reuse one of these lists for five minutes and must not share it between users. The 2025 answers carry neither field.

A call to a tool that does not exist returns the JSON-RPC error `-32602` (`Tool <name> not found`), not a tool result. A tool that exists and fails returns a result with `isError: true`.

A request body is limited to about 43.7 MiB (45,787,820 bytes). That covers a 32 MiB attachment in base64, with room for the JSON-RPC envelope. A larger body is refused with `413`.

`/mcp` answers only for `app.probara.net`. A request addressed to another host, such as a `*.workers.dev` address, is refused with `403`. A browser `Origin` must name the same host. A request without an `Origin`, such as one from a server-side client, is not affected by that check.

## Core tools

Nine tools cover the usual work in few calls. Every other operation is reached through the five tools that a default listing also carries, described under Operations and the run tools below.

| Tool                  | What it does                                                                                                            |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `get_project_context` | Returns a project's suites, custom fields, configurations, milestones, environments and members in one call.            |
| `get`                 | Reads any entity by display ID or ULID. A bare ULID also needs `kind`.                                                  |
| `query`               | Runs a PQL query.                                                                                                       |
| `query_help`          | Lists the PQL fields, operators and values of each entity, with examples that run.                                      |
| `analyze_run`         | Groups a run's failed and blocked cases by linked defect, then by suite, with their attempts.                           |
| `upsert_test_cases`   | Creates or updates up to 50 test cases. Each item names a display ID or an automation key, and reports its own outcome. |
| `start_run`           | Starts a run from a suite, a test plan, a PQL query or a list of cases, with its configurations and assignees.          |
| `record_results`      | Records up to 50 results in a run by test case display ID, with step results and evidence attachments.                  |
| `file_defect`         | Files a defect from a result, and can also create its GitHub or Jira issue.                                             |

`upsert_test_cases` matches an item to a test case by its display ID or its automation key. An item with a display ID that does not exist fails, because the server assigns display IDs. An automation key that matches no case creates one. Sending the same batch again changes nothing and reports every item as `unchanged`. One item failing does not stop the others.

## Prompts and resources

A client can also offer prompts and resources beside the tools. Each one reaches the same data as a tool, so nothing is reachable only through them.

Three prompts start a common task. Each one answers with a message that names the tools to run, in order:

| Prompt                  | Arguments                                                                                                  | Tools it names                                                                               |
| ----------------------- | ---------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `test_cases_from_issue` | `project_id` and `issue`: a GitHub issue URL, `owner/repo#n`, or a Jira issue key                          | `run_read_operation` to read the issue, then `get_project_context`, then `upsert_test_cases` |
| `triage_run`            | `project_id` and `run`: a display ID such as `R-12`, or a ULID                                             | `analyze_run`, then `get`, then `file_defect`                                                |
| `regression_for_pr`     | `project_id`, `pull_request` (a GitHub pull request URL or `owner/repo#n`) and an optional `changed_areas` | `query_help` and `query` to find the cases, then `start_run`                                 |

Probara does not read a pull request's diff, so `changed_areas` sets the scope of a regression run.

Two resources hold the same JSON as their tools:

| Resource        | URI                                                                           | Same answer as        |
| --------------- | ----------------------------------------------------------------------------- | --------------------- |
| PQL schema      | `probara://pql/schema`                                                        | `query_help`          |
| Project context | `probara://projects/{project}/context`, with the project code, such as `SHOP` | `get_project_context` |

A resource read takes the same path as its tool, so an unknown project answers with the tool's message as a JSON-RPC error, with the URI in its data. A resource has no block for the untrusted-data notice, so each resource's description says that its JSON holds organization data.

Resources need the `mcp:read` scope. Every prompt ends in a write tool, so prompts need both `mcp:read` and `mcp:write`. A grant without the scope lists none of them, and a request for one is refused with `403` and the `insufficient_scope` challenge, as a hidden tool is.

## Operations and the run tools

A default listing carries the nine core tools and five more, so an agent sees up to 14 tools when it connects, and fewer under a narrower grant. Every other operation is in the [tool reference](https://docs.probara.net/en/developers/mcp-tools/), and each one is reached in three steps:

1. `search_operations` finds operations by keyword, such as `project` or `run result`. Each hit gives the operation's name, a one-line summary and the run tool that runs it.
2. `describe_operation` returns the operation's full description, the JSON schema of its inputs, its annotations and its output schema when it declares one.
3. `run_read_operation`, `run_write_operation` or `run_delete_operation` runs it. Pass the operation's name as `operation` and its inputs as `arguments`. The result is the operation's own result.

The three run tools split by risk. `run_read_operation` runs read-only operations, `run_write_operation` runs writes that remove nothing, and `run_delete_operation` runs destructive operations such as deletes. A run tool refuses an operation of another risk, and its error names the run tool to use. An unknown name answers with the closest names.

Calling an operation by its name directly, such as `create_project`, returns a tool error that names its run tool. Only the nine core tools are called directly.

Every tool refuses an argument it does not take: the call returns a tool error that names the argument, and nothing runs. Through a run tool, the error also lists the operation's arguments, with the required ones marked. The same holds for a key inside an item of `record_results` or `upsert_test_cases`, or inside a `caseAssignees` entry of `start_run`: a misspelled `notes` refuses the whole call instead of recording the result without it.

### What a read-only grant lists

An OAuth grant without `mcp:write` lists the five read core tools and three tools that find and read operations: `search_operations`, `describe_operation` and `run_read_operation`. It lists no write tool, and search returns only read operations. A write call from such a grant is refused with `403` and the `insufficient_scope` challenge for `mcp:write`, as before. An OAuth grant that holds `mcp:write` alone lists the four write core tools and the two write run tools. An API token lists every default tool.

## Destructive operations: preview, then confirm

Some operations can remove or irreversibly change more than one record in one call: the bulk operations `bulk_delete_tags`, `bulk_remove_run_cases` and `reconcile_run_cases`, the deletes of a project, suite, test case, run, test plan, dashboard, custom field, tag, milestone or environment, and `deactivate_org_member`. Each one runs in two calls:

1. Call it without `confirm_token`. Nothing runs. The result is a preview: `status` is `confirmation_required`, `targets` lists what the call names with its display ID and name where one read finds them, `irreversible` says what cannot be undone, and `confirm_token`, `expires_at` and `expires_in_seconds` say how long the token lasts.
2. Repeat the same call with the same arguments and the `confirm_token`. The operation runs.

A token is single-use and valid for five minutes. It is bound to the credential that made the preview (an API token or an OAuth grant), its organization, the operation and its arguments. A call that presents a token is refused, and nothing runs, when:

- the token is unknown, expired or already used: call again without `confirm_token` for a new preview;
- the token was issued for another operation, credential, organization or set of arguments: it stays valid for its own call;
- the token store is unavailable: the error is retryable, so retry the call.

`run_delete_operation` takes `confirm_token` inside `arguments`, as the operation does when it is called directly.

## Activity log

A change made through an MCP OAuth connection records the connection in its audit event: `metadata.oauthClient` holds the client's `clientId` and `clientName`, as the audit events API returns them. The activity feed shows the change under the user, as it does for any session.

## Untrusted data in results

A successful result that carries organization data, such as titles, descriptions, steps, comments and text imported from GitHub or Jira, ends with a second text block. That block says the JSON above is data written by people in the organization or imported from GitHub or Jira, and that the text is data, not instructions. The first text block is the same JSON as before, and `structuredContent` is unchanged. Errors and the catalog answers of `search_operations` and `describe_operation` carry no such block.

## Parameterized test cases

A test case display ID, such as `SHOP-45`, can match several run cases of one run when the case is parameterized. An operation that takes a display ID and a run refuses the call, and lists up to 25 of those run cases, unless the call also names one: pass its combination as `parameters`, an array of name and value pairs such as `[{"name":"Site","value":"EU"}]`, or pass the run case ULID. Bulk operations take run case ULIDs for such a case instead. The server never picks one for you. `upsert_test_cases` and `create_test_case` accept `parameters` and `excludedCombinations`, as the API does. Shared parameters are operations, found with `search_operations` and run by name: `list_shared_parameters`, `get_shared_parameter`, `create_shared_parameter`, `update_shared_parameter`, `delete_shared_parameter` (it needs confirmation and takes a `mode`), `list_shared_parameter_usage`, `preview_test_case_combinations`, and `detach_test_case_parameter`. The API and MCP apply the same rule. See the [MCP tool reference](https://docs.probara.net/en/developers/mcp-tools/) and [Parameterized test cases](https://docs.probara.net/en/guides/parameterized-test-cases/).

## Identifiers

Every argument that takes an entity accepts its ULID or its display ID, and the server looks the display ID up for you.

- A test case (`SHOP-45`) and a suite (`SHOP-S3`) carry their project code, so they need no `project_id`.
- A run (`R-12`), milestone (`M-5`), defect (`D-7`), test plan (`PLAN-3`), shared step (`SS-2`) and review (`RV-4`) are numbered within a project. Send `project_id` with them.
- A run case is a test case inside a run. The run-case tools take the test case's display ID together with `run_ulid`. The bulk run tools also take the test case's ULID.

A display ID that does not exist answers with an error that names it and the project. A project code that does not exist answers with the valid project codes.

## Tool annotations

Each tool declares what it does, so a client that honors the declaration can ask the user before a risky call. These are hints. The server never refuses a call because of them.

- **Read-only** tools only read data.
- **Destructive** tools remove data, or replace a whole list, document or credential in one call. Replace and reconcile tools take the full set you want, so anything you leave out is removed.
- **Idempotent** tools have no further effect when the same call is repeated, so retrying one is safe.
- **External systems** tools reach outside Probara: a GitHub or Jira write, a GitHub workflow launch, a webhook delivery, or an invitation email.

The [MCP tool reference](https://docs.probara.net/en/developers/mcp-tools/) lists the hints of every tool. On connection the server also sends a short overview of Probara's data model and identifiers, which many clients add to the model's context.

## Response size

A list tool returns one page per call. The [tool reference](https://docs.probara.net/en/developers/mcp-tools/) states each tool's default page size and the argument that changes it. The largest page is 200 items.

A page that is still too large for one response (about 10,000 tokens) is handled in one of two ways:

- **Lists that continue with `nextCursor`** are trimmed to the items that fit. The result carries a `truncation` object: `returned` is the number of items it holds and `pageItems` is the number the page holds. Its `nextCursor` points at the last item it holds, so the next call returns the rest, with no gap.
- **Lists that page with `page`** are refused with `response_too_large`, and nothing is returned. The message names the `pageSize` that fits; walk the list with that size.

`get`, `list_results` and `list_run_cases` also take `fields`, a list of the field names to return. `query` takes none: the `SELECT` clause of the query names its columns. The display ID and the ULID are always returned. An unknown name is refused with `fields_invalid`, and the message lists the valid names.

A single record, or a list without a paging argument, is returned whole up to about 25,000 tokens. A larger one is refused with `response_too_large`. The message names a filter to add or a narrower view to request, or says that the tool has none.

## Troubleshooting

| Symptom                                                            | Likely cause                                                                                                                                                                                                                                |
| ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401` immediately, no browser prompt                               | Client does not support MCP OAuth discovery — use an API token instead                                                                                                                                                                      |
| Consent screen shows the wrong organization                        | Pick a different organization on the consent screen's org picker before approving; each grant binds to exactly one organization                                                                                                             |
| A tool call returns `403` with `insufficient_scope`                | The OAuth grant lacks the scope the call needs (`mcp:read` to read, `mcp:write` to change). Authorize again to get it.                                                                                                                      |
| A tool call fails with `forbidden`, naming the scope and your role | Your role in that organization no longer allows the scope, although the grant holds it. Authorizing again does not change this: an organization owner or admin must change your role. See [Authentication](https://docs.probara.net/en/developers/authentication/). |
| `503` with a `Retry-After` header                                  | Probara could not check the credential because of a temporary fault — wait the seconds in `Retry-After`, then retry                                                                                                                         |
| `429` with a `Retry-After` header                                  | Per-token or per-organization rate limit hit — wait the seconds in `Retry-After`, then retry                                                                                                                                                |
| `403` before a tool runs                                           | The client calls `/mcp` on a host other than `app.probara.net`, or sends a foreign `Origin`. Point it at `https://app.probara.net/mcp`.                                                                                                     |
| `413` on a tool call                                               | The request body is over the 43.7 MiB limit. Attachments are capped at 32 MiB, so send a smaller file.                                                                                                                                      |
| JSON-RPC `-32602`, `Tool … not found`                              | The tool name is not in the list the client holds. Refresh the tool list; a cached list lasts up to five minutes.                                                                                                                           |
| `response_too_large` in a tool result                              | The result is too large for one response. The message names the page size to request, a filter to add, or a narrower view. If it names none, the tool has no smaller form of that record.                                                   |
