Skip to main content

HTTP and SSE API reference

This is the detailed operator and wire reference. For the client-integration entry point, shared event lifecycle, and gRPC comparison, start with Drive via gRPC / HTTP.

The HTTP adapter wraps the same service. Every event is emitted as one SSE data: line carrying the generated proto Go value marshalled by encoding/json — so field names match the gRPC event shape, while protobuf enums are JSON numbers rather than protojson enum names.

Server identity

GET /v1/info?provider_id=<active-provider> is a process-wide, state-free identity probe. It takes no request body. provider_id is optional but must be the caller's already-known active provider; it may appear exactly once. Absent, repeated, or unknown selectors leave llm_provider_display_endpoint unavailable. It returns 200 OK with this JSON object:

{"build_id":"dev","server_implementation":"mecated","llm_provider_display_endpoint":"https://api.example/v1"}

build_id is the opaque linker-stamped build identity (dev in an unstamped source build). server_implementation is the stable composition family only: mecated, mecak8s, or embedded mecatui; a generic embedding uses unknown. It is neither an instance identifier nor a deployment label.

The optional llm_provider_display_endpoint is a sanitized diagnostic display projection for the supplied provider_id, or absent when unavailable; it is not connection configuration or a connection instruction. It retains only scheme, host, optional port, and escaped clean path; userinfo, query, fragment, invalid/control data, malformed values, and oversized values are omitted. The route does not infer a default or session selection, discover providers, re-read configuration, inspect session state, or list alternatives.

The route is inside the normal API authentication boundary. When --auth-token (or MECATL_AUTH_TOKEN) is configured, send its bearer token exactly as for the other HTTP API routes; configured TLS or mTLS requirements also apply. A caller without required authentication receives the normal 401 response. The endpoint never reads or returns configuration beyond this sanctioned endpoint projection, capabilities, topology, listener or connection details, authentication or TLS material, workspace paths, session or durable state, prompts, credentials, or raw errors.

The route is additive. An older server returns 404; clients should reduce that to their own safe unsupported status rather than display the response body. Clients must tolerate an absent or blank server_implementation in an otherwise valid response as unknown, and preserve an unfamiliar non-empty family for forward compatibility. build_id is not a semantic-version API.

Sessions & runs:

Method & pathBodyResponse
POST /v1/sessions{mode?, limits?, provider_id?, model_id?, profile?, mcp_servers?}; profile omitted = server default, "no-fs" = explicit attenuation201 {session_id, placement} where placement is bounded display metadata; no path or exact private ref
GET /v1/sessions200 {sessions: [...]} — path-free stored-session inventory
GET /v1/sessions/{id}200 authoritative session snapshot, including title/provenance, title-generation lifecycle, and canonical durable token usage when present
GET /v1/sessions/{id}/events200 text/event-stream — replay a session's durable event log (including session.title changes and the log-only approval/compaction_archive/user_prompt a live prompt stream skips); empty for an unknown id, 501 when no durable EventLog is wired
GET /v1/sessions/{id}/watch?cursor=&run_id=200 text/event-streamdurable replay-then-follow (ADR 0250). Each data: frame is {event, cursor, phase} (NOT a bare Event like /events); phase is an open string replay/live/gap. Exactly one event-less live frame marks the replay→live boundary; an event-less gap frame marks a failed durable append. cursor is opaque — empty means the beginning; hand back the last one you PROCESSED to resume. Optional run_id narrows delivery to one run; a cursor is scoped to the run_id it was issued under — resume with the same filter, or from the beginning, since a filtered watch's position advances past the records it dropped. The stream STAYS OPEN (unlike /events, which ends). 501 when no durable EventLog or no cursor seam, 404 when the caller may not read the session, 400 for a delegation-child session id. A cursor fault is not a status code on this route: the cursor is decoded after the 200 is committed, so a malformed or expired cursor arrives as the same terminal frame everything else does (cursor_malformed / cursor_expired); over gRPC it is a status. A mid-stream fault arrives as a final SSE frame tagged event: error whose data: line carries {"code","error"}watch_lagging is resumable (reconnect with your last cursor), activity_gap means recorded events are missing
POST /v1/sessions/{id}/rename{title}200 updated session snapshot with operator title provenance; 412 when kind/state/liveness gates reject the stale action, 409 when another replica holds the session lease
POST /v1/sessions/{id}/delete204 after permanently removing the snapshot and store-managed sidecars; 412 when the target is active, awaiting, or not a main chat, 409 when another replica holds the session lease, 501 when the configured store cannot physically delete
POST /v1/sessions/{id}/compactno body200 {"compacted":true} when one forced pass saved shorter model history, or {"compacted":false} for a successful no-op; 412 for an active/awaiting/non-main session, 409 when another replica holds its lease
POST /v1/sessions/{id}/workspace-enrollment/connectno body200 safe WorkspaceEnrollment projection; begins an eligible pre-prompt workspace-service enrollment or observes its exact pending enrollment
POST /v1/sessions/{id}/workspace-enrollment/{enrollment_id}/retryno body200 safe WorkspaceEnrollment projection; cancels the exact pending enrollment before beginning its replacement; stale IDs return 412
POST /v1/sessions/{id}/workspace-enrollment/{enrollment_id}/cancelno body200 safe WorkspaceEnrollment projection; cancels only the exact pending enrollment and clears its prompt gate; stale IDs return 412
DELETE /v1/sessions/{id}204 — close the session, releasing its per-session resources (not physical stored-session deletion)
POST /v1/sessions/{id}/prompt{text}200 text/event-stream of events; rejected while failed-step retry intent is pending
POST /v1/sessions/{id}/retryno body200 text/event-stream for a prompt-free failed-step retry; reuses conversation/tool state but re-resolves live instruction sources; 409 unless persisted state is eligible
POST /v1/sessions/{id}/approve{ask_id, allow}204
POST /v1/sessions/{id}/plan:approve{"target_mode": "default" | "accept_edits" | "plan", "note": "..."}200 text/event-stream — atomically resolve a parked plan-approval ask (ADR 0069): on default/accept_edits resume the parked run AND start the continuation run (both streamed); on plan/"" iterate (no continuation). 409 on a precondition failure (live run / not awaiting / not a plan ask), 404 on an unknown session
POST /v1/sessions/{id}/cancel204
POST /v1/sessions/{id}/cancel-child{child_id}204; 404 for an unknown / already-finished child
POST /v1/sessions/{id}/steer{text?, parts?, message_id?, expected_run_id?}; text or at least one part is required200 {outcome, message_id?, promoted?, run_id?}
POST /v1/sessions/{id}/cancel-steeroptional {message_id?, expected_run_id?}200 {outcome, message_id?}
POST /v1/sessions/{id}/clear{"worktree_selector":"..."} optional201 {session_id, placement} — distinct empty-history successor; omitted selector inherits exact source placement
POST /v1/sessions/{id}/forkoptional {title, reasoning_effort, provider_id, model_id, worktree_selector}201 {session_id, placement} — history-carrying successor; omitted selector inherits exact placement, supplied selector must be fresh and source-scoped; all overrides resolve atomically

WorkspaceEnrollment contains only enrollment_id, status, required_services, and the ephemeral presentation_url when a new enrollment needs browser presentation. These unary controls reject every request body, validate the session and enrollment correlation from the path, retain the request context, and do not expose callbacks, selectors, or broker state.

mcp_servers mounts client-provided streaming-HTTP MCP servers for the created session's lifetime, via a per-session engine. Each entry is {name, url, type?, headers?} — the HTTP mirror of the gRPC CreateSessionRequest.mcp_servers field, documented in full in the gRPC API guide. The short version: client MCP is a separate listener-scoped outbound-network/credential policy (ADR 0248). Only a --grpc-unix-socket daemon with --http-addr "" accepts it — which means the HTTP surface never does, since serving HTTP at all is a TCP listener; every other deployment, loopback included, returns 501 / client_mcp_unsupported. Check mcp_servers_on_create in GET /v1/compatibility features first. Mounting is all-or-nothing: a server that does not connect fails the create with 503 / client_mcp_unreachable rather than returning a session quietly missing its tools. A stdio or sse entry is 400 on every deployment (Mecatl never spawns an MCP server process); each name must be 1-64 chars of [A-Za-z0-9._-], contain no __, and be unique in the request; a URL carrying userinfo credentials is 400 (use headers); and header values are never logged, evented, or echoed in an error.

The create body is decoded strictly. An unrecognized field is 400 naming the field, rather than being silently ignored. This matters most for mcp_servers: the protojson spelling mcpServers used to be dropped, returning 201 for a session with none of the requested servers. It applies to every field on the body, so a client sending stray keys that previously succeeded now gets a 400.

Placement is server-owned. The create body has no workspace, cwd, placement ID, exact EnvironmentRef, or worktree selector. Local/embedded --workspace is trusted server configuration only. GET /v1/commands?session_id=... and GET /v1/worktrees?session_id=... authorize and exactly reattach that source session; no-FS returns empty without touching filesystem providers. Worktrees contain bounded display metadata and an opaque caller/source-scoped selector accepted only by the clear and fork successor routes. Selectors are not paths or durable IDs and expire on server restart, so clients relist. Sessions/snapshots and trusted driver storage retain the exact private EnvironmentRef{kind,id,revision}; HTTP projections never do.

Inventory & introspection (the HTTP mirrors of the gRPC inventory RPCs in §9):

Method & pathResponse
GET /v1/modelsthe selectable provider/model inventory (ListModels)
GET /v1/agentsthe agent-definition inventory
GET /v1/skillsthe skills inventory
GET /v1/commands?session_id=...slash commands for an owned, exactly reattached source; no-FS returns empty
GET /v1/worktrees?session_id=...display-safe worktrees plus ephemeral caller/source-scoped selectors; no paths/exact refs
GET /v1/soulthe resolved soul snapshot (provenance, trust, drift)
GET /v1/usermodelthe live bounded user-model index; ?key=<exact-key> also returns read-only value/version/provenance/proposal linkage/timestamps/bounded history when available
POST /v1/sessions/{id}/reflectsynchronously reflect a caller-owned completed session on its persisted provider (optional empty/{} body); returns bounded abstained/staged/promoted/conflicted counts
GET /v1/learning/attemptscontent-free caller-partitioned attempt page; accepts closed state, opaque cursor, and bounded limit (default 50, maximum 200)
GET /v1/learning/attempts/{id}content-free attempt lifecycle detail; foreign and missing IDs both return 404
POST /v1/learning/attempts/{id}/retrybody {"expected_version":"<opaque>"}; failed-to-queued attempt CAS only
POST /v1/learning/attempts/{id}/abandonbody {"expected_version":"<opaque>"}; non-compensating attempt CAS only, with no downstream rollback promise
POST /v1/dream/plansbody {"target":"project_memory"} or {"target":"user_model"}; spends one planner call and returns the bounded-lifetime exact/synthesized review plan plus opaque process-local id
POST /v1/dream/plans/{plan_id}/decisionbody {"decision":"apply"} or {"decision":"dismiss"}; decides the authoritative retained whole plan and returns planned/applied/conflicted/skipped/failed source counts
GET /v1/learning/proposalsbounded caller-partitioned proposal page (status, cursor, limit, optional reviewable project; promotion remains launch-root/trust-gated)
GET /v1/learning/proposals/{id}bounded proposal detail with digest availability, never raw evidence text
POST /v1/learning/proposals/{id}/decisionapprove/reject with expected_version; stale versions return 409
POST /v1/learning/proposals/{id}/undocompensating undo with expected_version; stale/current-memory conflicts return 409
GET /v1/mcp/resourcesMCP resource snapshots
GET /v1/mcp/resources/readread one MCP resource by URI
GET /v1/mcp/promptsthe MCP prompt inventory
POST /v1/mcp/prompts/getexpand one MCP prompt (rendered messages)
GET /v1/mcp/sourcesthe resolved MCP source inventory
GET /v1/mcp/toolhive/groupsthe ToolHive groups in the resolved inventory

Manual dream generation sends the selected bounded memory values/descriptions to the configured planner and spends tokens; regeneration is explicit and spends again. Apply/dismiss is whole-plan, with no client-supplied operations or per-source toggles. Exact duplicates keep the survivor; approved synthesis atomically rewrites the displayed survivor and tombstones the displayed sources per operation, while independent operations can yield a partial receipt. Plans expire after ten minutes and exist only on the generating process: expiry, restart, or a wrong replica returns 404 and makes the old decision non-retryable while allowing explicit fresh generation. A same-decision request while apply is running returns 409 and remains explicitly retryable; an opposite request returns 412 while apply is active and 410 after the opposite terminal decision. Only the terminal case offers fresh generation. Genuinely indeterminate transport failures preserve the exact plan ID and decision for same-decision retry because the first request may already have applied. No error path offers the opposite decision. Capacity pressure returns 429, and an unavailable deployment/target 501. Ownership enforcement disables manual dream review. This does not change the separate, off-by-default schedule flags or learning.mode, and no recall counters or provider/model identity are returned.

Agent teams (with --enable-teams, the default):

Method & pathBodyResponse
POST /v1/teamsteam spec (incl. the tighten-only max_team_tokens?)create a team
POST /v1/teams/{id}/membersmember specspawn a teammate
POST /v1/teams/{id}/messagesmessagepost into a member's inbox
POST /v1/teams/{id}/members/cancel{"member": "..."}cancel one member of a running team (404 unknown team/member, 412 not running)
POST /v1/teams/{id}/runtext/event-stream of TeamEvents, ending with the terminal outcome frame
GET /v1/teams/{id}team snapshot (roster, tasks, quiescence)
DELETE /v1/teams/{id}clean up the team

All examples below were captured against a live mecated serve --mock.

Create a session

$ curl -s -X POST http://127.0.0.1:8081/v1/sessions -d '{}'
{"session_id":"8867bdea940108c1dd82d13d3fb7fc61","placement":{"kind":"local"}}

Optional fields:

{
"mode": "plan",
"limits": { "max_turns": 20, "max_tool_calls": 80, "max_consecutive_failures": 3 }
}

mode accepts default, plan, acceptedits (also accept_edits / accept); unknown/empty falls back to the server default (default). Each omitted or zero limits field inherits its deployment default:

LimitDeployment default
max_turns2000
max_tool_calls8000
max_consecutive_failures5

A non-zero field overrides only that limit; other zero fields still inherit their defaults.

A workspace, cwd, placement ID, or exact environment ref is an unknown field and the strict decoder returns 400; configure local --workspace on the server.

Create a no-filesystem session (profile: "no-fs")

A session can opt out of the filesystem entirely — useful for pure research/coordination agents (MCP tools + memory + web fetch) that should never touch a disk:

$ curl -s -X POST http://127.0.0.1:8081/v1/sessions \
-d '{"profile":"no-fs"}'
{"session_id":"..."}

The same profile field exists on the gRPC CreateSessionRequest (enum-as- string: "" = default, "no-fs"). Rules, all enforced server-side:

  • "no-fs" binds the server's filesystem-free placement; no workspace field exists. Omitted profile binds the server's deployment default.
  • Any other profile value is rejected loudly — never a silent fallback.
  • The no-FS session has no Read/ListDir/Edit/Write/Copy/Move/Remove/Grep/Glob/Shell/ShellStatus, no Parallel, and no SkillDraft. It keeps MCP tools (server-global + resource meta-tools + client MCP), the six memory tools, WebFetch, WebSearch, Skill (bodies are text injection; out-of-workspace skill assets are unreadable), and delegation — Subagent and Team children run the same file-less surface with no worktree/fork isolation (there is nothing to isolate) and no shell.
  • The model is told up front (a system-prompt posture note plus an honest Subagent tool description), so it plans around MCP/memory/web search+fetch instead of burning turns on unknown-tool errors.
  • The profile composes with provider_id/model_id and is FIXED for the session lifetime.

Inspect a session

$ curl -s http://127.0.0.1:8081/v1/sessions/8867bdea940108c1dd82d13d3fb7fc61
{"session_id":"8867bdea940108c1dd82d13d3fb7fc61","state":"idle","mode":"default","placement":{"kind":"local"},"turns":0,"tool_calls":0}

A missing id returns 404 {"error":"not found: \"...\""}.

Compact model history without a turn

Use the bodyless manual operation when the next prompt may not fit or when you want to reduce stored model history before continuing:

$ curl -s -X POST http://127.0.0.1:8081/v1/sessions/<id>/compact
{"compacted":true}

The server runs the configured compactor once regardless of the automatic 0.8 trigger. It adds no prompt and starts no model turn, although the cascade strategy may make a compaction-slot summarization call. false is a successful no-op and causes no save or event append. The legal states are idle, completed, cancelled, and failed; state is preserved. Running/awaiting, scheduled, child, or same-process live sessions return 412. A lease held by another replica returns 409. Missing and foreign-owned IDs both return 404; configured HTTP authentication still applies before ownership checks.

On change, the compacted snapshot is saved before the existing compaction notice and archive are appended. Save failure returns 500 without appending them. An event-log append failure after save does not roll back the snapshot or change the 200 response, so the log may lack that manual compaction record. Clients can check capabilities.manual_compaction on session creation; an old server leaves it false and returns 404 for the unknown route.

Start a run (SSE stream)

$ curl -s -N -X POST http://127.0.0.1:8081/v1/sessions/8867bdea940108c1dd82d13d3fb7fc61/prompt \
-d '{"text":"hello"}'
data: {"type":"turn.start","seq":1}

data: {"type":"message.delta","seq":2,"text":"Mock provider: no real model is configured. Set --openai/OPENAI_API_KEY for live use."}

data: {"type":"result","seq":3,"result":{"stop":"end_turn","text":"Mock provider: no real model is configured. Set --openai/OPENAI_API_KEY for live use.","usage":{}},"usage":{}}

-N disables curl's buffering so you see events as they stream. The example above is the --mock provider (one text turn). Against a real model you also see tool.call, tool.result, and — for tools that need approval — permission.ask. The JSON field names follow the proto JSON shape: tool_call, tool_result, ask ({ask_id, tool, args, reason}), is_error, call_id.

Disconnecting the client (closing the curl connection) cancels the run. text is required — omitting it returns 400 {"error":"text is required"}.

Retry the failed model step

When a terminal result explicitly carries retry_disposition: 2 (retryable) and stream_progress: 2 (precommit) or 3 (visible), repeat that exact model step without submitting another prompt. The concrete SSE mappings are retry_disposition: 0 unspecified, 1 unknown, 2 retryable, 3 permanent; and stream_progress: 0 unspecified, 1 unknown, 2 precommit, 3 visible, 4 complete. For example, an automatically safe retry result contains "retry_disposition":2,"stream_progress":2.

$ curl -s -N -X POST http://127.0.0.1:8081/v1/sessions/<id>/retry
data: {"type":"session.init","seq":1}

# model events follow, ending with a terminal result

The route has no request body. It persists failed-step retry intent before launch and adds no user message. Persisted conversation/tool state is reused while live turn-0 instructions, operator profile, and system-prompt sources are re-resolved. Normal /prompt requests are rejected while intent is pending. A clean pre-turn brake leaves it pending; cancellation clears it. A 409 means the server rejected eligibility. Retrying after VISIBLE output is an explicit operator choice; automated clients should use the narrower typed RETRYABLE + PRECOMMIT case and a finite retry bound.

The terminal Result fields are optional for wire compatibility. Presence with UNKNOWN is an explicit conservative answer from a new server. Absence means the server predates typed semantic retry and is not evidence that replay is safe.

Approve / deny a pending ask

When the stream emits a permission.ask with an ask.ask_id, resolve it on a second connection while the SSE stream is still open:

$ curl -s -X POST http://127.0.0.1:8081/v1/sessions/<id>/approve \
-d '{"ask_id":"<ask_id-from-the-event>","allow":true}'
# 204 No Content

Set "allow":false to deny (the model receives the denial reason and adapts). If there is no in-flight run for the session you get 404 {"error":"no in-flight run for session"}.

Approve a presented plan (plan:approve)

In plan mode, once the model has presented a complete plan it calls the PresentPlan signalling tool, which parks the run awaiting on a plan-approval ask (ask.tool == "PresentPlan" — the tool name is the discriminator; no provenance field on the proto). Resolve it atomically with POST /v1/sessions/{id}/plan:approve (ADR 0069):

$ curl -s -N -X POST http://127.0.0.1:8081/v1/sessions/<id>/plan:approve \
-d '{"target_mode":"default","note":"looks good, proceed"}'
# 200 text/event-stream — the resumed run's events, then (on allow) the
# continuation run's events carrying the proceed message, ending with a
# terminal result {stop: "plan_approved"} then the continuation's result.

target_mode selects the verdict and the resulting posture:

  • "default" → allow-once: flip to default mode (deny→ask→allow) and execute.
  • "accept_edits" → allow-always: flip to acceptEdits mode (auto-accept edits for the execution phase).
  • "plan" or "" → deny/iterate: stay in plan mode, NO continuation run starts (the model re-plans on the next prompt).

A 409 means a precondition failed: the session has a live run (use the Converse resume_approval frame for an in-flight run), is not awaiting, or is awaiting a non-plan ask. A 404 means an unknown session. An in-flight Converse run that parked on the plan ask may ALSO be resolved by the resume_approval frame on its own stream; the plan:approve RPC is the headless/cross-process composition of resume + continuation into one stream.

Cancel a run

$ curl -s -X POST http://127.0.0.1:8081/v1/sessions/<id>/cancel
# 204 No Content

The run terminates with a result whose stop is cancelled. No in-flight run → 404 {"error":"no in-flight run for session"}.

Steer a running session

A steer injects operator input into an in-flight run. The run commits it at the next turn boundary, after any current model response and tool batch settle. HTTP clients use the unary steer and cancel-steer routes. gRPC clients use the steer and steer_cancel arms on the bidirectional Converse stream. ACP does not support steer.

Check for both the runtime steer capability and the http_steer compatibility feature before using the HTTP routes. The feature prevents clients from probing older servers by 404.

Send text, multimodal parts, or both:

$ curl -s -X POST http://127.0.0.1:8081/v1/sessions/<id>/steer \
-H 'Content-Type: application/json' \
-d '{"text":"Use the existing parser","message_id":"client-42","expected_run_id":"<run-id>"}'
{"outcome":"accepted","message_id":"client-42"}

The parts array uses the same {kind, mime_type, data?, url?} content blocks as an HTTP prompt. The selected provider must support every supplied media kind. message_id is an optional client correlation value. It must be no longer than 64 Unicode code points. A pending bundle can accept multiple steers: the first returns accepted, and later fragments return appended. The eventual steer event echoes the latest message ID as a watermark for the committed bundle.

expected_run_id makes the operation strict. If that run is absent, has finished, or has been replaced, the server returns 409 with the problem code stale_run_control. It never promotes a strict steer to another run.

Without expected_run_id, a steer that loses the terminal race is promoted to a new follow-up run. The response remains JSON, not SSE:

{"outcome":"too_late","promoted":true,"run_id":"<new-run-id>"}

The server drains and records the promoted run in the background. Use the session watch API with the returned run ID when the client needs its events.

Retract a pending bundle before it reaches a turn boundary:

$ curl -s -X POST http://127.0.0.1:8081/v1/sessions/<id>/cancel-steer \
-H 'Content-Type: application/json' \
-d '{"message_id":"cancel-42","expected_run_id":"<run-id>"}'
{"outcome":"retracted","message_id":"cancel-42"}

An empty cancel-steer body is valid and unqualified. It returns {"outcome":"none_pending"} when no bundle can be retracted. A qualified cancel-steer returns 409 stale_run_control when the named run is absent or no longer active.

Both HTTP routes decode JSON strictly. Unknown fields, trailing JSON, a missing steer payload, invalid content, and an overlong message_id return 400. Bodies larger than 32 MiB return 413. Stale strict controls return 409.

ACP over stdio (mecated acp)

mecated acp serves the Agent Client Protocol — JSON-RPC 2.0 over stdin/stdout — for an editor that spawned mecated as a subprocess. It is the stdio alternative to the gRPC/HTTP listeners (which are skipped); everything else is the same wiring: the engine, tools, permission policy, session store, MCP, and skills come from the same app.Build assembly. ACP session/new binds the trusted configured placement and session/load exactly reattaches the persisted private ref before access. The editor-provided cwd is only a local consistency assertion; a mismatch is rejected and cwd cannot select or construct authority. Other ACP session, discovery, error, and event projections remain path-free. Logs go to stderr, so stdout carries only JSON-RPC frames.

  • There is no TLS / auth / rate limiting on this surface — stdio to the parent process is itself the trust boundary.
  • Prompt content is multimodal and capability-gated: every content block becomes text, a media part, or a loud invalid-params error (image/audio is gated on the session provider's capabilities) — never a silent drop.
  • ACP session resume (session/load) is advertised only when a durable session store is configured (--store-dir); the in-memory default would lose the session across a restart, so the capability is withheld.