---
title: "Run one headless agent turn"
method: POST
path: "/projects/{projectId}/agent"
tags: ["Agent"]
---

# Run one headless agent turn

`POST /projects/{projectId}/agent`

Runs ONE assistant turn over the supplied message history and responds synchronously with the final assistant text, the operations it invoked, references to any resources it created, and any actions awaiting human approval.

The caller owns conversation state: resend the full history each turn. The model is pinned server-side (hosted catalog) and billed to the project.

Tools are TIERED. Read operations and non-spending writes execute directly. Three categories are PROPOSAL-ONLY: operations that SPEND once (running a suite or a case, generating cases, cancelling a run), operations whose spend RECURS (setting a suite's schedule), and operations whose effects MCPJam cannot describe or undo because they leave MCPJam entirely (`call_server_tool`, which runs a tool on the caller's own MCP server). For each, the turn validates against the operation's real schema, persists a proposal, and returns it in `proposedActions` for a person to approve; `confirmSeverity` says which hazard the approver should be warned about. MCPJam-side deletions are excluded from the surface entirely — a proposal makes spend deliberate, but it does not make a deletion recoverable. Proposals are only offered when the request supplies a `conversationId`; without somewhere to collect a click there is nothing to approve, so the tools are withheld rather than offered and then refused.

Every tool invocation is hard-clamped to the path `projectId`. Turns are capped at 4 concurrent per organization (`429 RATE_LIMITED`; enforced per server instance) and ~90s wall clock (`504 TIMEOUT`). Guest callers are denied. Requires a hosted MCPJam deployment (`422 FEATURE_NOT_SUPPORTED` otherwise).

When a turn fails or times out AFTER work already persisted, the error body's `details.createdResources` and `details.proposedActions` carry what survived — check them before retrying.

**Retry policy:** send `idempotencyKey` as a STABLE identity for the triggering event (not a fresh uuid per attempt). Every write the turn performs derives its own key from it, so a retried turn's mutations land on the rows the first attempt created instead of duplicating them. The key makes a retry SAFE; it does not make one free — dedupe at your own trigger so most retries never re-run the turn at all. A retry whose model authors materially different arguments hashes differently and is correctly treated as a different write.

## Path parameters

- `projectId` string, required

## Request body

- AgentTurnRequest
  - `messages` object[], required — The conversation so far, oldest first. The caller owns state and resends the full history each turn. Each message content is capped at 8,000 characters AND 8,192 UTF-8 bytes; the whole history is additionally capped at 98,304 UTF-8 bytes (96 KB).
    - `role` 'user' | 'assistant', required
    - `content` string, required — Plain text. Max 8,000 characters and 8,192 UTF-8 bytes.
  - `idempotencyKey` string — A STABLE identity for the triggering event — not a fresh uuid per attempt. Each write the turn performs derives its own key from this one, so a retried turn re-issues the same mutations onto the same rows instead of authoring duplicates. Printable ASCII only (the value becomes a header on every write the turn issues). Omitting it preserves the older non-idempotent behaviour rather than being rejected.
  - `conversationId` string — Where an approval control can be rendered for this turn (a channel, a thread, a DM). Its presence is what makes the GATED tools available at all: without somewhere to collect a click, the turn is not offered them, so the model never proposes an action nobody can approve.
  - `slackChannelId` string — Slack-named spelling of `conversationId`. Both are accepted indefinitely; `conversationId` wins when both are sent.

## Response `200`

The completed turn.

- AgentTurnResponse
  - `reply` string, required — The assistant's final text for this turn.
  - `toolCalls` object[], required
    - `operation` string, required — Platform operation name invoked during the turn.
  - `createdResources` object[], required — Resources the turn created, with app deep links.
    - `type` string, required — Resource type. New types may be added; render an unknown type generically rather than dropping it.
    - `id` string, required
    - `name` string
    - `url` string, uri, required
  - `proposedActions` object[], required — Actions the turn wants to take but may not take on its own — they SPEND, or reach a third-party system. Nothing here has happened: each entry is a persisted proposal awaiting a human click. The `actionId` is all an approval control needs to carry, because the server holds what the action does; the remaining fields are for RENDERING only and must never be echoed back as instructions.
    - `actionId` string, required — Opaque id. The only value a click may carry.
    - `operation` string, required — Platform operation name. Display and telemetry only.
    - `description` string, required — Short summary of the TARGET, not a cost estimate. Model-influenced text — escape before rendering.
    - `buttonLabel` string, required — Verb for the approval control, e.g. "Run it".
    - `kind` 'start' | 'cancel' | 'generate' | 'schedule' | 'external', required — What approving it does, so a host can word its confirmation and its after-the-fact announcement truthfully. Treat an unrecognised value as neutral rather than guessing.
    - `confirmSeverity` 'spend' | 'external' | 'none' — How stern the confirmation copy should be: `spend` consumes quota or credits, `external` runs somewhere MCPJam does not control and cannot undo, `none` states explicitly that the default cost warning would be FALSE for this action (disabling a schedule stops spend). Absent means the host's default copy is honest enough.
    - `target` object — What the proposal is about, for correlating it with other turn output (e.g. suppressing a duplicate run affordance on the created suite it already offers to run). Display/dedup only — never an instruction. Absent means match-unknown; fall back to coarser behavior.
      - `type` string, required — Resource type the proposal is about, e.g. `eval_suite`.
      - `selector` string, required — The validated input's own selector for it — an id where the server minted the proposal, possibly a name where the model authored it. Match against both.
  - `usage` object, required
    - `inputTokens` integer, required
    - `outputTokens` integer, required

## Other responses

- `400` — Malformed body or parameters.
- `401` — Missing, invalid, revoked, or orphaned key (`UNAUTHORIZED`) — or the **target MCP server** needs an OAuth grant (`OAUTH_REQUIRED`), which is a property of the server, not your key.
- `422` — The target server doesn't support this MCP capability.
- `429` — Per-key rate limit exceeded (60 requests/minute sustained, bursts up to 10). Honor `Retry-After` and back off with jitter.
- `500` — Something failed on MCPJam's side.
- `504` — The target MCP server connected but didn't respond in time.

---

[API](https://skmtc.net/mcpjam/apis/mcpjam-api.md) · [All operations](https://skmtc.net/mcpjam/apis/mcpjam-api/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/mcpjam/mcpjam-api/versions/b767df188d0c/schema)
