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

# MCP: connect a coding agent or an assistant

> The MCP endpoint, the read-only tools and the scope each needs, protocol versions, answers, errors and paging, and what is not available over MCP.

`POST /v1/mcp` is a JSON-RPC 2.0 [Model Context Protocol](https://modelcontextprotocol.io) endpoint. A coding agent or assistant can read its workspace and, with the matching permissions, create and edit agents, save versions, publish drafts, run offline tests and simulations, manage text knowledge and bind owned phone numbers. Every tool runs the matching API handler as the caller. The write registry cannot place a call, send an SMS, buy a number or delete a resource. The separately scoped phone tools require the approvals described below. Write access does not bypass roles, workspace boundaries, quotas or release checks.

The original read registry contains 29 tools, with separate registries for write, draft-and-test and phone operations.

## Connect

Use the same bearer key as every other route ([Authentication and credentials](https://docs.sempera.org/authentication.md)), or sign in with OAuth (below). `agents:read` is the door, and a phone scope opens it too for a credential that holds only those: anything else is refused at the endpoint (`403 insufficient_scope`). Each tool then names the one further scope it needs, and `tools/list` shows a key only the tools its scopes allow. A CI key is refused (`403 forbidden`), as it always was. The call below asks for the setup snippet of one client; the same answer is a plain read at `GET /v1/mcp/clients/{client}`.

```bash theme={null}
curl -X POST https://api.sempera.org/v1/mcp \
  -H "Authorization: Bearer sempera_sk_..." \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"mcp_client_setup","arguments":{"client":"claude-code"}}}'
```

The endpoint takes one JSON message per POST and answers one JSON body. It offers no event stream and no session: with a live credential, any method but `POST` (`GET`, `HEAD` and `DELETE` among them) answers `405` with `Allow: POST`, and it sends no `Mcp-Session-Id`. That is the plain JSON form of the Streamable HTTP transport, which every published MCP revision lets a server choose and requires a client to accept, so ChatGPT and Claude need no event stream to connect. No tool here sends progress, so none is offered.

## Sign in with OAuth

A host that cannot hold a static key (ChatGPT, Claude) signs in with OAuth 2.1 instead. Give it the URL `https://api.sempera.org/v1/mcp`. Bearer keys keep working exactly as before.

1. The host posts without a token and gets `401` with `WWW-Authenticate: Bearer resource_metadata="https://api.sempera.org/.well-known/oauth-protected-resource/v1/mcp"`.
2. It reads that document, then `https://api.sempera.org/.well-known/oauth-authorization-server`.
3. It registers itself at `POST /v1/oauth/register` (RFC 7591) and sends the browser to `GET /v1/oauth/authorize` with PKCE (`S256`, required), the `resource` `https://api.sempera.org/v1/mcp`, and the scopes it wants. Redirects match exactly. A loopback address may differ by port alone, because a native client binds a free port each time.
4. The person signs in on `app.sempera.org` if they have not, and a workspace owner approves on the consent page: which client, which workspace, which scopes, and where the answer goes. A client Sempera cannot recognise by its published redirect is shown as unverified. The owner can approve fewer scopes than were asked for, never more.
5. The host swaps the code at `POST /v1/oauth/token`. An access token lasts one hour and a refresh token 30 days. Each refresh turns the refresh token over. Presenting a spent one ends every token from that sign-in, and so does presenting a spent code.

| Scope | What it allows |
| - | - |
| `phone:read` | See the line, the allowance, and the results of calls and texts |
| `phone:call` | Start and cancel approved calls |
| `phone:text` | Send approved texts and read their threads |

These three are all a client can ever hold. No scope buys a number, a plan or credits: a request naming any other scope is refused `invalid_scope`, and a client that asks for nothing gets `phone:read`. Scopes open only the tools that need them, so a phone-only grant does not see agent tools.

A token works on `POST /v1/mcp` and nowhere else (any other route answers `401`), and only for the resource it was issued to. It resolves to the same principal a key does, so the same role, scope, workspace and audit checks apply. Only `SHA-256` hashes of tokens, codes and client secrets are stored.

Client Metadata Documents are not offered. A host that prefers them falls back to registration, which ChatGPT, Claude and Codex all document. Tools do not yet ask for more scope mid-conversation: to change what a client holds, approve it again, which replaces its grant.

Owners see every client in Settings, Connected clients (`GET /v1/client-grants`), with its scopes and last use, and end one with `DELETE /v1/client-grants/{grant_id}`. Its tokens stop at once and every other client keeps working.

## Protocol versions

Two revisions are spoken. Which rules a request is held to depends on one thing: whether it carries the `MCP-Protocol-Version` header.

| A request that... | What it gets |
| - | - |
| carries `MCP-Protocol-Version: 2025-06-18` or `MCP-Protocol-Version: 2024-11-05` | The Streamable HTTP rules for a server with no stream and no sessions. A notification, or a client's answer to a request, is accepted with `202` and no body. A body without `jsonrpc: "2.0"`, a request without an `id`, or a JSON array is refused with `400`. The two headers are served identically; what `initialize` answers depends only on the revision named in its own parameters. |
| carries `MCP-Protocol-Version` naming any other revision, empty or not | `400 validation_failed`, with the revisions spoken listed in `supported`. Before the endpoint negotiated, the header was ignored. |
| carries no `MCP-Protocol-Version` header | Served as this endpoint always served it: a notification gets `200` and an empty result, and a JSON array is a `400`. Only three of its answers change; they are listed under Which clients move. |

`initialize` answers the revision the client asked for when it is `2025-06-18` or `2024-11-05`. For a newer date (`2025-11-25`, `2026-07-28` and later) it answers `2025-06-18`, the newest spoken; for `2025-03-26`, an older date, an unknown value or none it answers `2024-11-05`, which is all it answered before it negotiated. Resources, prompts, completions, logging, structured output, event streams and sessions are not offered; nothing here claims them.

With either supported version header, an `id` must be a string or a finite integral number. Numeric spellings such as `1`, `1.0` and `1e0` are accepted and the parsed value is echoed without conversion. Null, booleans, objects, arrays, fractional numbers and non-finite values are refused before a tool is called, with HTTP `400`, JSON-RPC `-32600` and a null response id. Notifications still omit the id and receive `202` with no body; valid client replies receive the same empty acknowledgement. Requests without the version header retain their legacy behavior.

### Which clients move

* **A client that sends the header.** The revisions from `2025-06-18` on require a client to send `MCP-Protocol-Version` on every request after `initialize`, so a client built on a current SDK sends it. Such a client now negotiates `2025-06-18` (it asks for a newer revision and is answered with the newest spoken), then gets `202` for its `notifications/initialized`, and may try to open a stream with a `GET` on the endpoint and be answered `405`, which that revision allows for a server that offers none. This is a change from the endpoint before this release, which answered such a client `2024-11-05` and `200`. A client that sends a header naming any other revision is refused with `400`.
* **A client that sends no header on any request.** Three of the answers it already got change: `initialize` asking for `2025-06-18` or newer is answered `2025-06-18` (it was `2024-11-05`); `tools/list` with a `cursor` is a `-32602` error (it was ignored, and the list never paged); and each of the three original tools gains `title`, `annotations` and `_meta` in `tools/list`. Its `tools/list` also now carries the 26 added tools. Every other answer to it is unchanged, including the original tools' results and their `-32000` errors.
* **A browser page on another site.** See the `Origin` rule below. The API answers a browser's preflight for this endpoint with `401` and no CORS headers, so such a page could not send it a JSON request with a key in the first place; the `Origin` rule is a second lock.

## Origin

Every request that carries an `Origin` header is checked, with or without `MCP-Protocol-Version` (the only method served is `POST`; any other is refused `405`, or `401` with no live credential, before it gets that far). The `Origin` value must be this deployment's own host, a host the deployment is configured to allow, or an origin the App is served from (the origins the deployment lists for its web App); otherwise the answer is `403 forbidden`, and the request's scope, version header and body are never looked at. The App's own pages therefore reach this endpoint through the App's proxy even when that proxy runs on another host or port than the API, as it does in development and in the browser tests.

The API's front door looks at the credential first: a request with no key, an unknown key or a revoked key is refused `401` whatever its `Origin`, as is a dead token, and that `401` says where to sign in, so the `403` is what a request with a live key and a foreign `Origin` gets. A request with no `Origin` header (a command-line tool, an SDK, a server-side client) is not a browser's and is not checked. `Origin: null`, an empty `Origin` and a value that is not a URL are foreign, and so is a request with several `Origin` headers when any one of them is. The deployment's own host means the host and port the request arrived with, so that half relies on the deployment's host policy (which answers `404` to a host it does not know) and does not compare the scheme; an App origin is compared whole, scheme, host and port. This rule is new with the release that added these tools: a client that sent an `Origin` of another site and was served before is refused now.

## Tools

Every tool projects an existing route, run as the caller: the route's own scope, role, workspace and ownership checks apply. The table below lists the original read tools; the write workflow and its follow-up reads are below. An id from another workspace is refused. The `list_agents` tool keeps its original shape; a list that its route pages is paged here the same way.

| Tool | Reads | Scope | Arguments |
| - | - | - | - |
| `list_agents` | `GET /v1/agents` | `agents:read` | none |
| `get_agent` | `GET /v1/agents/{agent_id}` | `agents:read` | agent\_id (required) |
| `mcp_client_setup` | `GET /v1/mcp/clients/{client}` | `agents:read` | client (required) |
| `get_agent_draft` | `GET /v1/agents/{agent_id}/draft` | `agents:read` | agent\_id (required) |
| `list_agent_versions` | `GET /v1/agents/{agent_id}/versions` | `agents:read` | agent\_id (required), limit, cursor |
| `get_agent_version` | `GET /v1/agents/{agent_id}/versions/{version_id}` | `agents:read` | agent\_id (required), version\_id (required) |
| `get_latest_agent_version` | `GET /v2/agents/{uuid}/versions/latest` | `deploy:read` | agent\_id (required) |
| `list_agent_environments` | `GET /v2/agents/{uuid}/environments` | `deploy:read` | agent\_id (required) |
| `list_agent_variables` | `GET /v2/agents/{uuid}/environments/{env}/variables` | `deploy:read` | agent\_id (required), environment (required) |
| `list_agent_branches` | `GET /v2/agents/{uuid}/branches` | `agents:read` | agent\_id (required) |
| `list_agent_deployments` | `GET /v2/agents/{uuid}/deployments` | `deploy:read` | agent\_id (required) |
| `list_agent_check_runs` | `GET /v2/agents/{uuid}/check-runs` | `evals:read` | agent\_id (required), limit |
| `list_calls` | `GET /v1/calls` | `calls:read` | agent\_id (required), limit, cursor |
| `get_call_log` | `GET /v1/calls/{call_id}` | `calls:read` | call\_id (required), agent\_id |
| `list_sessions` | `GET /v1/sessions` | `sessions:read` | agent\_id (required), channel, limit, cursor |
| `get_session` | `GET /v1/sessions/{session_id}` | `sessions:read` | session\_id (required), agent\_id |
| `list_knowledge_sources` | `GET /v1/knowledge-sources` | `knowledge:read` | agent\_id (required), limit, cursor |
| `get_knowledge_source` | `GET /v1/knowledge-sources/{source_id}` | `knowledge:read` | source\_id (required), agent\_id (required) |
| `list_tools` | `GET /v1/tools` | `tools:read` | status, limit, cursor |
| `get_tool` | `GET /v1/tools/{tool_id}` | `tools:read` | tool\_id (required) |
| `list_integrations` | `GET /v1/integrations` | `integrations:read` | none |
| `list_tests` | `GET /v1/tests` | `tests:read` | agent\_id (required), limit, cursor |
| `get_test_run` | `GET /v1/tests/{test_id}` | `tests:read` | test\_id (required) |
| `list_eval_agents` | `GET /v1/evals/agents` | `evals:read` | limit, cursor |
| `get_eval_agent` | `GET /v1/evals/{eval_id}` | `evals:read` | eval\_id (required) |
| `get_eval_results` | `GET /v1/evals/results` | `evals:read` | agent\_id (required) |
| `list_voices` | `GET /v1/voices` | `agents:read` | none |
| `get_docs_page` | `GET /docs/{page}` | none | page (required) |
| `get_docs_index` | `GET /llms.txt` | none | none |

Each tool in `tools/list` carries MCP's own hints (`readOnlyHint` true, `destructiveHint` false, `idempotentHint` true, `openWorldHint` false) and Sempera's explicit metadata under `_meta`: `org.sempera/effect` (`read`), `org.sempera/side-effects` (`false`), `org.sempera/scope`, `org.sempera/route`, `org.sempera/operation-id` and `org.sempera/paginated`. A host should treat hints as claims by the server, which is why the server also enforces them: a tool can only project a `GET` route. `openWorldHint` is false because no tool sends a value you gave it to a third party; a read may still do what its route does inside Sempera, such as the cached health check of Sempera's own speech engines behind `list_voices`.

## Arguments, answers and errors

* An agent is named `agent_id` in every tool, whichever path variable the route calls it. Ids are letters, digits, `.`, `_`, `:` and `-`, up to 200 characters. An unknown argument, a missing required one, a wrong type or an out-of-range number is refused before anything is read, with the argument named.
* An answer is the route's JSON as text in one `text` content item (narrowed as a later section says for nine tools), or, for the two guide tools, its Markdown. A failure is a tool result with `isError: true` whose text is the route's own error envelope, for example `{"error":{"code":"not_found","message":"no such agent","request_id":"req_..."}}`. The codes are those in [Errors and limits](https://docs.sempera.org/errors-and-limits.md), and `request_id` is the one the call returned in `X-Request-Id`. An answer about your workspace never carries an internal address, the name of a setting, a file path, a stack trace or the name of an engine or supplier behind Sempera; the tests scan every tool's answer for them.
* Arguments that are not a JSON object, an unknown method and an unknown cursor on `tools/list` are protocol errors (`-32602`, `-32601`). The three original tools keep reporting a refusal as JSON-RPC error `-32000`, as they always did; every newer tool reports it as a tool result.
* Lists page with `limit` (1 to 200, default 50; `list_agent_check_runs` takes 1 to 100) and `cursor`. Pass `next_cursor` from the last page back unchanged to read the next one (`nextCursor` on `list_tools`, `data.next_cursor` on `list_eval_agents`). A cursor is an opaque position: the same cursor gives the same page while the rows are unchanged, and one issued to another key reads only that key's own rows. `list_integrations`, `list_voices` and the environment, branch and deployment reads answer in one piece.
* An answer larger than 1 MiB is refused with `payload_too_large`; ask for a smaller page or a more specific id. A read that runs longer than 30 seconds is refused with `service_unavailable`. A call counts once against the key's request budget ([Errors and limits](https://docs.sempera.org/errors-and-limits.md)), like any other request.
* A secret environment variable always reads `null` in `list_agent_variables`; its value never leaves the server.

### What nine tools leave out

Nine tools answer a short, written-out list of fields rather than the route's whole answer. A field the route gains later is not passed on until this page and the tool both name it. The REST routes are unchanged.

* **Calls and conversation records** (`list_calls`, `get_call_log`, `list_sessions`, `get_session`) return metadata only: the id, the agent, the channel, the status, the start and end times, the duration, the language, the outcome as a code and a label, the version and the number of turns. There is no caller number (not even a masked one), no recording, no transcript, no summary and no title (a phone call's title is its summary), no extracted values and no provider log. The outcome, the language and the version come from the voice platform's record of the call, so each is shown only when it has the shape of a short code (letters, digits and `.` `_` `:` `-`, no spaces); anything else reads `null`, and an outcome's label is shown only when it is one of Sempera's own outcome labels or the plain wording of its code. No argument switches any of these on. Reading what was said needs a transcript permission that does not exist yet.
* \*\*`get_eval_results`\*\* returns counts, pass rates, the weekly and per-version trend, and one row per scored conversation with its verdict and score. It leaves out the judge's reasoning (which quotes the conversation), the failure clusters built from it, the judge's rubric and the evaluator's own configuration.
* \*\*`list_voices`\*\* takes no arguments. It lists the built-in voices an agent may name, with each one's language and whether it may carry a live call, followed by your workspace's own ready custom voices. It asks no outside voice library and takes no search, language or provider filter. No other workspace's voice is ever listed.
* \*\*`list_tests` and `get_test_run`\*\* read legacy battery jobs in memory (at most 20 newest), plus candidate checks retained in the App's canonical Checks ledger on Sempera's servers. A retained candidate is resolved by id only among the caller's authorized agents after restart. Legacy jobs are lost at restart. A run shows its state and outcome, not the failure text of a run that crashed nor who started it. Another workspace's run is answered `not found` with the same sentence as an unknown id.
* \*\*`list_agent_check_runs`\*\* returns each check run's id, agent, version, status and times. A run that ended in an error keeps the status `ERROR`, and its `error_message` is always the one sentence "The run ended in an error. Its detail is not returned over MCP." in place of the system's own words for what went wrong, which can name a file on a server, a setting or an address.

## Phone tools

A credential with `phone:read`, `phone:call` or `phone:text` also lists `phone_get_profile`, `phone_preview_call`, `phone_start_call`, `phone_get_task`, `phone_cancel_task`, `phone_preview_text`, `phone_send_text` and `phone_get_thread`. They run the same operations as `/v1/phone-*` and refuse with the same codes. Each tool needs its own scope. A preview returns a link for the signed-in owner, and a start or a send runs only once the owner approved it or a standing approval covers it. No tool approves, buys, subscribes or tops up. See [Personal phone plan](https://docs.sempera.org/phone-plan.md).

## Not available over MCP

* Rolling back, archiving or deleting an agent, discarding a draft, buying or releasing a number, running a paid evaluation, changing a variable or a tool. Calls are available only through the separately scoped and approved phone tools described above. The write registry is an explicit list, not arbitrary HTTP access.
* The text of a call, the caller's number and a call's summary, until a separate permission for them exists.
* Integration detail by key. `list_integrations` answers; there is no tool for one integration.
* Post-call event subscriptions, resources and prompts. None of these exist yet, and no page here documents them as live.

## Draft and test over MCP

Three tools run the same write routes the App and REST clients use, as the caller, with the route's own scope, role, workspace and agent checks. A draft edited here is the one canonical draft the App opens; there is no second copy. They are listed in `tools/list` only to a credential that holds their scope.

| Tool | Runs | Scope | Arguments |
| - | - | - | - |
| `create_agent` | `POST /v1/agents` (owner role) | `agents:write` | name (required), template, idempotency\_key (required) |
| `update_agent_draft` | `PATCH /v1/agents/{agent_id}/draft` | `agents:write` | agent\_id (required), fields (required), expect\_revision (required), idempotency\_key (required) |
| `run_agent_tests` | `POST /v1/tests` | `tests:write` | agent\_id (required), scenarios, candidate\_revision, runtime\_policy, idempotency\_key (required) |

* `idempotency_key` is the request's identity: a retry with the same value gets the first answer back and changes nothing again; the same value with a different change is refused `idempotency_key_reused`.
* `update_agent_draft` needs `expect_revision`, the `revision` that `get_agent_draft` returned. An edit made from an older read is refused `conflict` with `reason: stale_draft`, so a newer change by someone else is never overwritten.
* `run_agent_tests` with `candidate_revision` and `runtime_policy` runs the reviewed draft through the App's canonical Checks runner, never a real call, booking, SMS or email. Without both it retains the legacy battery behavior, but a paid or unknown configured runtime is refused. Read the run with `get_test_run`.
* Every call writes one row to the workspace's audit log (`mcp.tool_call`: the tool, the agent, the outcome, the credential id; never the text you sent) and each completed change one builder usage row under `mcp:<tool>` (a refused call or a replayed retry is not counted), apart from the call minutes the call service records.

## Connected AI clients

An owner connects one outside client at a time in Settings, API keys, Connected AI clients (or `POST /v1/mcp/connections` with `client`, `access` and optional `call_records` and `expires_in_days`). Each connection is its own key, shown once, that:

* works on `POST /v1/mcp` and nothing else (any other route answers `403 forbidden`);
* holds a fixed scope list: `read` gives the read tools' scopes (call and conversation metadata only with `call_records`), `draft` adds only `agents:write`, `test` adds only `tests:write`, and `build` adds both. Owners can authorize drafting without test execution, or testing without draft edits. Explicit `write` access also adds `agents:publish`, `knowledge:write`, `phone_numbers:read` and `phone_numbers:write`. It never holds `calls:write` or a key scope. Existing keys are not upgraded;
* expires (90 days by default, at most 365) and is listed with when it was last used.

`GET /v1/mcp/connections` lists them and `DELETE /v1/mcp/connections/{connection_id}` disconnects one. Disconnecting revokes that key only: the deployed agents keep answering calls exactly as before. Another workspace's connection, and an ordinary API key, read as `not_found` on these routes.

## Write tools and a safe agent recipe

Choose Write in Settings, API keys, Connected AI clients and confirm the permission change. This creates a new expiring key, not an upgrade of an existing connection. Ordinary API keys can use these tools when their scopes and roles allow them. A client should ask its operator to approve publishing and number binding before invoking those tools.

Every write requires `idempotency_key`. Reuse it only for an identical retry. A timeout is not proof that nothing changed: retry with the same key and read the resource before attempting another change.

| Tool | Method and path | Scope | Arguments besides idempotency\_key |
| - | - | - | - |
| `save_agent_version` | `POST /v1/agents/{agent_id}/versions` | `agents:write` | agent\_id |
| `publish_agent_version` | `POST /v1/agents/{agent_id}/publish` | `agents:publish` | agent\_id, reason |
| `run_agent_simulations` | `POST /v1/agents/{agent_id}/simulations` | `tests:write` | agent\_id, scenario\_keys, optional repeats |
| `bind_phone_number` | `POST /v1/phone-numbers` | `phone_numbers:write` | agent\_id, number |
| `create_knowledge_source` | `POST /v1/knowledge-sources/text` | `knowledge:write` | agent\_id, name ending in .txt, content |
| `update_knowledge_content` | `PATCH /v1/knowledge/{uuid}/content` | `knowledge:write` | source\_id, content, expect\_version\_id |
| `reparse_knowledge_source` | `POST /v1/knowledge-sources/{source_id}/reparse` | `knowledge:write` | agent\_id, source\_id |

Follow-up reads are `get_knowledge_content` (`source_id`, `knowledge:read`, `GET /v1/knowledge/{uuid}/content`) and `get_agent_simulation` (`agent_id`, `simulation_id`, `tests:read`, `GET /v1/agents/{agent_id}/simulations/{simulation_id}`). A created knowledge source returns both its library `id` for reprocessing and its `content_id` for reading and editing text. Read `data.version_id` before editing and supply it as `expect_version_id`; stale edits are refused.

1. Connect with the narrowest access needed and call `tools/list` to discover available tools.
2. Use `create_agent` with a synthetic business name, then `get_agent_draft` and `update_agent_draft` with the returned revision. The App reads the same draft.
3. Add a text source with `create_knowledge_source`. Use the draft's editable attachment fields to attach its collection. Uploading alone does not attach it or change the live agent.
4. Run `run_agent_tests` on the draft and read `get_test_run`. A failed or unavailable runner is not a passed test.
5. With explicit operator approval, use `publish_agent_version` to apply the draft and create an immutable version. `save_agent_version` instead compiles the already approved specification without applying unpublished edits. Neither tool promotes a production environment.
6. Run `run_agent_simulations` with chosen scenario keys and read `get_agent_simulation` until it finishes. The normal offline runner and its required local dependencies must be installed. Simulation results do not establish live-call proof.
7. Only after the normal telephony ownership and entitlement requirements are met, use `bind_phone_number` for an already owned number. Purchasing and releasing numbers are not MCP tools.

The new writes carry `readOnlyHint: false`, explicit effect metadata, and the same audit and usage accounting as the draft tools. Publishing is marked `org.sempera/publishes: true`. Live client interoperability, production database behavior and carrier ownership checks require separate authorized proof; local tests do not claim those.

## Build and test an agent from an outside AI client over REST

A client that does not speak MCP can do the same over the REST API, with a key an owner of the workspace creates in Settings, API keys. The steps below need `agents:write` to create and edit a draft and to try it with a typed preview, and `tests:write` to run the test battery. The key reads results back with the matching `read` scopes. A key made in the App holds every scope a developer key may have, so it can also publish (`agents:publish`) and place a real outbound phone call (`calls:write`). Keep it private and do not paste it into a shared chat.

| Step | Method and path | Scope |
| - | - | - |
| Create a draft agent | `POST /v1/agents` with `{"name": "..."}` (optional `template`, `answers`) | `agents:write` |
| Read the draft and its revision | `GET /v1/agents/{agent_id}/draft` | `agents:read` |
| Edit the draft | `PATCH /v1/agents/{agent_id}/draft` with `{"fields": {...}, "expect_revision": "..."}` | `agents:write` |
| Add a test scenario to the draft | `POST /v2/agents/{agent_id}/scenarios` | `agents:write` |
| Try the draft with one typed message | `POST /v1/agents/{agent_id}/preview` with `{"message": "..."}`; it never places a call | `agents:write` |
| Run the test battery | `POST /v1/tests` with `{"agent_id": "...", "scenarios": ["..."]}`; it answers `202` and runs in the background | `tests:write` |
| Read a test run | `GET /v1/tests/{test_id}` (or the `get_test_run` MCP tool) | `tests:read` |

Send an `Authorization` header with the value `Bearer <api-key>`, replacing the placeholder with your key ([Authentication and credentials](https://docs.sempera.org/authentication.md)). A draft save that carries an old `expect_revision` is refused with `409 conflict`, so read the draft first and send its `revision`.

What a key cannot do:

* **Make another key.** Only an owner signed in to the App can, so a key cannot mint a narrower one.
* **Use the Builder.** The Builder's jobs (start, answer, review, apply) run inside the signed-in app. They are not on the API, and a key does not open them.

A connector that signs in with OAuth, such as a ChatGPT connector, uses the sign-in above. No real account has proven it yet ([Supported clients](https://docs.sempera.org/supported-clients.md)).

## Clients

`mcp_client_setup` returns the URL, the header and the config snippet for each client below. The protocol behavior on this page is covered by automated tests that play a client; no client build below has yet been run against the live endpoint, and the table says so.

| Client | Setup snippet | Run against the live endpoint |
| - | - | - |
| Claude Code | `mcp_client_setup` with `claude-code` | not yet tested (2026-10-09) |
| Claude Desktop | `mcp_client_setup` with `claude-desktop` | not yet tested (2026-10-09) |
| Cursor | `mcp_client_setup` with `cursor` | not yet tested (2026-10-09) |
| VS Code | `mcp_client_setup` with `vs-code` | not yet tested (2026-10-09) |
| Windsurf | `mcp_client_setup` with `windsurf` | not yet tested (2026-10-09) |
| Gemini CLI | `mcp_client_setup` with `gemini-cli` | not yet tested (2026-10-09) |
| Codex | `mcp_client_setup` with `codex` | not yet tested (2026-10-09) |
| Any other client | `mcp_client_setup` with `other` | a plain `curl` call, shown above |

## Retained candidate checks

Read `get_agent_draft`, review its canonical fields and diff, then pass its revision as `candidate_revision` to `run_agent_tests`. Also pass exactly `runtime_policy: {"evidence":"local-model","tools":"fixture","max_cost":0}`. The two arguments are a pair: a missing policy or revision, a live tool policy, a hosted-model request, a nonzero cost limit or unknown policy fields is refused. The configured hosted fallback is not inherited. If the local model is unavailable, the run fails honestly rather than switching to a paid model.

The candidate response identifies the reviewed draft revision, the SHA-256 of the actual compiled candidate and its parent AgentVersion. Its runtime policy is explicit and `release_evidence` is false. Check results are written to the existing App Checks ledger, so `list_tests` and `get_test_run` can review them after a process restart. This does not create a new builder, agent store, compiler or publication road. These retained runs need Sempera's persistent evidence store; a deployment without it is refused before a battery starts. The actual served model build still needs independent runtime evidence; the policy label is not proof of a model version or a quality pass.

Fixture batteries in automated tests prove the route, compiler, private tool facade, candidate binding, record retention and refusal behavior. They are not local-model quality evidence, live-model evidence, private host receipt or a production approval. ChatGPT authorization remains unavailable until the separately approved external grant and release gates are satisfied.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.