POST /v1/mcp is a JSON-RPC 2.0 Model Context Protocol 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), 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}.
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 URLhttps://api.sempera.org/v1/mcp. Bearer keys keep working exactly as before.
- The host posts without a token and gets
401withWWW-Authenticate: Bearer resource_metadata="https://api.sempera.org/.well-known/oauth-protected-resource/v1/mcp". - It reads that document, then
https://api.sempera.org/.well-known/oauth-authorization-server. - It registers itself at
POST /v1/oauth/register(RFC 7591) and sends the browser toGET /v1/oauth/authorizewith PKCE (S256, required), theresourcehttps://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. - The person signs in on
app.sempera.orgif 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. - 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.
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 theMCP-Protocol-Version header.
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-18on require a client to sendMCP-Protocol-Versionon every request afterinitialize, so a client built on a current SDK sends it. Such a client now negotiates2025-06-18(it asks for a newer revision and is answered with the newest spoken), then gets202for itsnotifications/initialized, and may try to open a stream with aGETon the endpoint and be answered405, which that revision allows for a server that offers none. This is a change from the endpoint before this release, which answered such a client2024-11-05and200. A client that sends a header naming any other revision is refused with400. - A client that sends no header on any request. Three of the answers it already got change:
initializeasking for2025-06-18or newer is answered2025-06-18(it was2024-11-05);tools/listwith acursoris a-32602error (it was ignored, and the list never paged); and each of the three original tools gainstitle,annotationsand_metaintools/list. Itstools/listalso now carries the 26 added tools. Every other answer to it is unchanged, including the original tools’ results and their-32000errors. - A browser page on another site. See the
Originrule below. The API answers a browser’s preflight for this endpoint with401and no CORS headers, so such a page could not send it a JSON request with a key in the first place; theOriginrule is a second lock.
Origin
Every request that carries anOrigin 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. Thelist_agents tool keeps its original shape; a list that its route pages is paged here the same way.
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_idin 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
textcontent item (narrowed as a later section says for nine tools), or, for the two guide tools, its Markdown. A failure is a tool result withisError: truewhose 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, andrequest_idis the one the call returned inX-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/listare 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_runstakes 1 to 100) andcursor. Passnext_cursorfrom the last page back unchanged to read the next one (nextCursoronlist_tools,data.next_cursoronlist_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_voicesand 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 withservice_unavailable. A call counts once against the key’s request budget (Errors and limits), like any other request. - A secret environment variable always reads
nullinlist_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 readsnull, 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_testsandget_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 answerednot foundwith 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 statusERROR, and itserror_messageis 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 withphone: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.
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_integrationsanswers; 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 intools/list only to a credential that holds their scope.
idempotency_keyis 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 refusedidempotency_key_reused.update_agent_draftneedsexpect_revision, therevisionthatget_agent_draftreturned. An edit made from an older read is refusedconflictwithreason: stale_draft, so a newer change by someone else is never overwritten.run_agent_testswithcandidate_revisionandruntime_policyruns 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 withget_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 undermcp:<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 (orPOST /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/mcpand nothing else (any other route answers403 forbidden); - holds a fixed scope list:
readgives the read tools’ scopes (call and conversation metadata only withcall_records),draftadds onlyagents:write,testadds onlytests:write, andbuildadds both. Owners can authorize drafting without test execution, or testing without draft edits. Explicitwriteaccess also addsagents:publish,knowledge:write,phone_numbers:readandphone_numbers:write. It never holdscalls:writeor 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 requiresidempotency_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.
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.
- Connect with the narrowest access needed and call
tools/listto discover available tools. - Use
create_agentwith a synthetic business name, thenget_agent_draftandupdate_agent_draftwith the returned revision. The App reads the same draft. - 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. - Run
run_agent_testson the draft and readget_test_run. A failed or unavailable runner is not a passed test. - With explicit operator approval, use
publish_agent_versionto apply the draft and create an immutable version.save_agent_versioninstead compiles the already approved specification without applying unpublished edits. Neither tool promotes a production environment. - Run
run_agent_simulationswith chosen scenario keys and readget_agent_simulationuntil it finishes. The normal offline runner and its required local dependencies must be installed. Simulation results do not establish live-call proof. - Only after the normal telephony ownership and entitlement requirements are met, use
bind_phone_numberfor an already owned number. Purchasing and releasing numbers are not MCP tools.
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 needagents: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.
Send an
Authorization header with the value Bearer <api-key>, replacing the placeholder with your key (Authentication and credentials). 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.
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.
Retained candidate checks
Readget_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.