---
title: "Send one agent Playground message"
method: POST
path: "/chat-sessions/messages"
tags: ["Chat sessions"]
---

# Send one agent Playground message

`POST /chat-sessions/messages`

Send one message to a project's MCP servers and get the model's reply plus the telemetry a participant in the conversation could not see: which tools ran, with what arguments, what each returned, per-call latency, and token usage.

**Spends model credits per call.** `idempotencyKey` is required and must be STABLE for the triggering intent — a fresh key per HTTP attempt deduplicates nothing, so a timeout-and-retry would run and bill the turn twice. With a stable key, a retry replays the completed turn.

Omit `sessionId` to start a session; pass the one this returns to continue it. Configuration (`modelId`, target, `systemPrompt`, `toolMode`) pins on the FIRST turn — a continuation that resends any of it is refused with `details.reason: "CONFIG_ON_CONTINUATION"`.

Only sessions created through this endpoint may be continued through it (`CONTINUATION_NOT_ALLOWED`): appending to a human's live Playground session would interleave two writers on one transcript.

`toolMode` defaults to `read_only`, which advertises only tools the server annotated `readOnlyHint: true`. That hint is server-asserted, so `read_only` is a policy this host applies, not a guarantee it can verify. `auto` advertises everything and **may cause real external side effects** through arbitrary third-party tools.

## Request body

- SendChatMessageRequest
  - `idempotencyKey` string, required — STABLE identity for this turn's intent — reuse it when retrying. A fresh key per attempt deduplicates nothing and will bill the turn twice. Printable ASCII only.
  - `message` string, required — The message to send, as the user.
  - `projectId` string — Required to START a session; ignored when continuing one.
  - `sessionId` string — Continue this session. Omit to start a new one.
  - `modelId` string — Provider-prefixed model id, e.g. `anthropic/claude-sonnet-5`. Required on a first turn. A BARE id is rejected with `details.reason: "MODEL_AMBIGUOUS"` rather than guessed — an unprefixed id is indistinguishable from a local Ollama model, and guessing would spend on the wrong rail.
  - `environmentId` string — Target this environment's servers. Mutually exclusive with `serverIds`. First turn only.
  - `serverIds` string[] — Target these project servers. Mutually exclusive with `environmentId`. First turn only. Server CONFIGS are never accepted — only ids the project already holds.
  - `systemPrompt` string — First turn only.
  - `temperature` number — First turn only — pinned to the session and reused on every continuation.
  - `maxSteps` integer
  - `toolMode` 'read_only' | 'auto' — `read_only` advertises only tools annotated `readOnlyHint: true`. `auto` advertises everything and MAY CAUSE REAL EXTERNAL SIDE EFFECTS. First turn only.
  - `allowedServerIds` string[] — Narrow THIS TURN to a subset of the target's servers. An empty array narrows to none and is rejected — omit the field to use the whole target. Per-turn, not pinned.
  - `allowedTools` string[] — Advertise only these tool names, for THIS TURN. An empty array advertises no tools at all — the same request as `maxToolCalls: 0`. Per-turn, not pinned.
  - `maxToolCalls` integer — Cap the tool calls this turn may make, enforced at DISPATCH rather than by bounding steps (one step can emit several parallel calls). `0` advertises no tools at all.

## Response `200`

The turn ran. `persisted.outcome` reports whether the transcript landed — a turn that ran but failed to persist still spent, so this is a 200 with an honest `persisted` block rather than an error.

- ChatTurn
  - `sessionId` string, nullable, required — The one public session id. Pass it back to continue, and to the trace/detail reads. NULL only when the turn ran but its transcript did not persist — which `persisted.outcome` reports, and which must not be read as "nothing happened": the turn already spent.
  - `turnId` string, required — Minted by the turn lease and used by the ingest dedupe, so it names the same turn in both.
  - `reply` string
  - `finishReason` string, nullable
  - `toolCalls` ChatTurnToolCall[]
    - `toolCallId` string, required
    - `toolName` string, required
    - `input` unknown, required
    - `status` 'ok' | 'error', required
    - `output` unknown
    - `errorMessage` string
    - `truncated` boolean — The payload was clipped. Always announced — a silently shortened result an agent believes is complete sends it debugging the wrong thing.
  - `trace` object — This turn's spans, inline.
    - `turnId` string
    - `spanCount` integer
    - `spans` object[]
  - `usage` TurnUsage
    - `inputTokens` integer
    - `outputTokens` integer
    - `totalTokens` integer
  - `model` object
    - `id` string
    - `provider` string
  - `toolMode` 'read_only' | 'auto'
  - `advertisedToolCount` integer — Tools the model could see this turn.
  - `excludedToolCount` integer — Tools the tool policy withheld.
  - `persisted` object, required
    - `outcome` string
    - `version` integer
  - `origin` 'api', required
  - `replay` boolean — Set when this idempotencyKey replayed an already-completed turn. Nothing was spent.
  - `message` string

## 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.
- `403` — Key is valid but not allowed to do this.
- `404` — Unknown project, server, or resource.
- `409` — `details.reason` is one of `TURN_IN_PROGRESS` (another turn holds this session's lease; retry after `details.retryAfterMs`), `CONTINUATION_NOT_ALLOWED`, or `SESSION_VERSION_CONFLICT`.
- `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.
- `502` — Could not connect to the target MCP server.
- `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/revisions/087a1084c020/schema)
