# Build a JSON-RPC client

> Client responsibilities: newline-delimited framing, request ids, handling server requests mid-turn, `initialized` notification ordering, and compliance-oriented `clientInfo.name` values.

- Repository: openai/codex
- GitHub: https://github.com/openai/codex
- Human docs: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1
- Complete Markdown: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/llms-full.txt

## Source Files

- `README.md`
- `src/message_processor.rs`
- `src/outgoing_message.rs`
- `src/request_processors/initialize_processor.rs`
- `tests/common/test_app_server.rs`
- `tests/common/responses.rs`

---

---
title: "Build a JSON-RPC client"
description: "Client responsibilities: newline-delimited framing, request ids, handling server requests mid-turn, `initialized` notification ordering, and compliance-oriented `clientInfo.name` values."
---

`codex app-server` speaks JSON-RPC–style messages over stdio (default), unix control socket, or experimental websocket listeners. A correct client owns transport framing, correlates request ids per connection, completes the `initialize` / `initialized` handshake before other RPCs, and continuously reads the stream so server-initiated requests and turn notifications are handled while work is in flight.

## Wire framing

| Transport | Framing rule |
| --- | --- |
| stdio (`--stdio`, `--listen stdio://`) | One JSON object per line (JSONL): serialize the message, append `\n`, flush |
| unix control socket | WebSocket upgrade, then one JSON-RPC message per websocket text frame |
| websocket (`--listen ws://…`) | One JSON-RPC message per websocket text frame (experimental) |

Codex omits the `"jsonrpc":"2.0"` field on the wire even though the protocol is JSON-RPC 2.0–like. Each line (or frame) must deserialize to exactly one of: `request`, `response`, `error`, or `notification`.

<Note>
The integration test harness implements the canonical stdio pattern: serialize with `serde_json`, write bytes plus `\n` to stdin, and `read_line` from stdout for each inbound message.
</Note>

## Message shapes

| Direction | JSON shape | `id` field |
| --- | --- | --- |
| Client → server request | `{ "id", "method", "params"? }` | Required |
| Server → client request | `{ "id", "method", "params" }` | Required (server-assigned integer) |
| Response | `{ "id", "result" }` | Matches originating request |
| Error | `{ "id", "error": { "code", "message", "data"? } }` | Matches originating request |
| Notification | `{ "method", "params"? }` | Omitted |

`RequestId` is either an integer or a string; pick one style per client and keep ids unique among in-flight client-originated requests on that connection.

## Request id rules

**Client-originated RPCs.** Allocate monotonic integer ids (the test harness starts at 0 and increments). When a response or error arrives, match `id` before treating the RPC as complete. The server scopes ids per connection; the same numeric id on two connections is independent.

**Server-originated RPCs.** The server assigns monotonic integer ids from its own counter. Store each pending server request by `id` until you send a JSON-RPC `response` with the same `id` and a typed `result` payload.

**Responses to server requests.** Your client sends a result object on the wire, for example:

```json
{ "id": 7, "result": { "decision": "accept" } }
```

Errors use the same `id` with an `error` object.

## Connection handshake

Per connection, run this sequence before any other method:

<Steps>
<Step title="Send initialize">
Call `initialize` with `clientInfo` and optional `capabilities` (experimental API, notification opt-out, attestation).
</Step>
<Step title="Read initialize result">
Wait for the matching `response` (or `error`). On success you receive `userAgent`, `codexHome`, `platformFamily`, and `platformOs`.
</Step>
<Step title="Send initialized">
Emit the client notification `{"method":"initialized"}` with **no** `params` field.
</Step>
<Step title="Start reading notifications">
After initialize completes, the server may emit connection-scoped notifications (for example `configWarning`, `remoteControl/status/changed`). Broadcast notifications are only delivered to connections marked outbound-ready after initialize.
</Step>
</Steps>

```mermaid
sequenceDiagram
    participant Client
    participant Transport
    participant MessageProcessor
    participant Outbound

    Client->>Transport: request initialize (id=N)
    Transport->>MessageProcessor: process_request
    MessageProcessor->>MessageProcessor: session.initialize(clientInfo)
    MessageProcessor->>Outbound: response initialize (id=N)
    Outbound-->>Client: response initialize
    Client->>Transport: notification initialized
    Note over MessageProcessor: process_notification (logged)
    MessageProcessor->>Outbound: configWarning / remoteControl (connection-scoped)
    Outbound-->>Client: notifications
    Client->>Transport: request thread/start (id=N+1)
```

<Warning>
RPCs sent before `initialize` completes on a connection return `"Not initialized"`. A second `initialize` on the same connection returns `"Already initialized"`.
</Warning>

### Initialize params

<ParamField body="clientInfo.name" type="string" required>
HTTP-header-safe client identifier. Drives compliance logging and, for most clients, process-global originator / user-agent suffix.
</ParamField>

<ParamField body="clientInfo.version" type="string" required>
Client version string appended to the user-agent suffix as `{name}; {version}`.
</ParamField>

<ParamField body="clientInfo.title" type="string">
Optional display title; not used for originator selection.
</ParamField>

<ParamField body="capabilities.experimentalApi" type="boolean">
When `true`, enables experimental methods and fields for this connection.
</ParamField>

<ParamField body="capabilities.optOutNotificationMethods" type="string[]">
Exact notification method names to suppress for this connection (no wildcards).
</ParamField>

<ParamField body="capabilities.requestAttestation" type="boolean">
When `true`, client must answer server `attestation/generate` requests.
</ParamField>

<RequestExample>
```json
{
  "method": "initialize",
  "id": 0,
  "params": {
    "clientInfo": {
      "name": "codex_vscode",
      "title": "Codex VS Code Extension",
      "version": "0.1.0"
    },
    "capabilities": {
      "experimentalApi": true,
      "optOutNotificationMethods": ["thread/started"]
    }
  }
}
```
</RequestExample>

<ResponseExample>
```json
{
  "id": 0,
  "result": {
    "userAgent": "codex_vscode/0.1.0 …",
    "codexHome": "/path/to/.codex",
    "platformFamily": "unix",
    "platformOs": "macos"
  }
}
```
</ResponseExample>

After the response, send:

<RequestExample>
```json
{ "method": "initialized" }
```
</RequestExample>

## Compliance-oriented `clientInfo.name`

`clientInfo.name` is validated as an HTTP header value before the server commits session state. Invalid values (for example embedded `\r`) are rejected with code `-32600` and message `Invalid clientInfo.name: '…'. Must be a valid HTTP header value.`

| `clientInfo.name` | Mutates global originator / user-agent suffix? | Typical use |
| --- | --- | --- |
| Product integrations (`codex_vscode`, `xcode`, …) | Yes — sets originator and `userAgent` prefix | Desktop / IDE hosts |
| `codex_app_server_daemon` | No — probe / daemon clients | Health checks, internal daemons |
| `codex-backend` | No — backend callers | Server-side automation |

<Info>
Enterprise integrations intended for the OpenAI Compliance Logs Platform should use a known client name. Contact OpenAI to register new production client names. See the Codex admin API reference for compliance logging context (linked from the app-server README).
</Info>

Official VS Code extension example name: `codex_vscode`.

## Event loop architecture

A production client runs **one reader task** and **one writer** (or serialized writes) per connection. Never block the reader waiting for a single RPC while a turn is active — notifications and server requests arrive interleaved with late responses.

```text
┌─────────────────────────────────────────────┐
│ read loop (stdout / socket)                 │
│  deserialize JSONRPCMessage                 │
│  ├─ notification → dispatch / buffer        │
│  ├─ request (server) → approval handler     │
│  ├─ response/error → complete client RPC    │
└─────────────────────────────────────────────┘
         ▲                          │
         │                          ▼
┌────────────────┐        ┌───────────────────┐
│ pending server │        │ pending client    │
│ requests[id]   │        │ requests[id]      │
└────────────────┘        └───────────────────┘
```

Buffer non-matching messages while waiting for a specific response or server request. Match by `id` or notification `method`, not arrival order alone.

## Server requests during an active turn

While `turn/start` is in flight, the server may emit:

- Turn/item **notifications** (`turn/started`, `item/started`, deltas, `turn/completed`, …)
- **Server-initiated requests** (approvals, MCP elicitation, attestation, dynamic tool calls, …)

### Command execution approval (representative flow)

| Order | Message | Client action |
| --- | --- | --- |
| 1 | `item/started` (`commandExecution`) | Render pending UI |
| 2 | `item/commandExecution/requestApproval` (JSON-RPC **request**) | Show approval UI |
| 3 | JSON-RPC **response** `{ "decision": "accept" \| … }` | Resume work |
| 4 | `serverRequest/resolved` | Clear pending state |
| 5 | `item/completed` | Show final status/output |

Integration tests assert `serverRequest/resolved` arrives before `turn/completed` for approval flows.

### Responding to a server request

1. Parse the incoming JSON-RPC `request` into the typed `ServerRequest` enum (generate bindings from `codex app-server generate-ts` or `generate-json-schema`).
2. Present UI scoped by `threadId` / `turnId` / `itemId` from params.
3. Send a JSON-RPC `response` with the **same** `id` and a method-specific result object.
4. Wait for `serverRequest/resolved` (or handle cleanup if the turn ends first).

Pending server requests for a thread are tracked server-side; on reconnect or subscribe the server can replay outstanding requests to that connection. Key UI state by `requestId` to avoid duplicate prompts.

### Turn transition cleanup

If a turn is interrupted or superseded before the user answers, the server cancels pending server requests with an internal error whose `data.reason` is `turn_transition_pending_request`. The server still emits `serverRequest/resolved` for lifecycle cleanup in those cases.

## Client RPC serialization

Some client methods are serialized per thread on the server. Your client may send parallel RPCs, but thread-scoped operations can be ordered server-side. Prefer awaiting `turn/completed` (or a turn error) before starting conflicting thread operations unless you know the method’s scope.

## Retryable overload

When ingress queues are saturated, new client requests receive:

| Field | Value |
| --- | --- |
| `error.code` | `-32001` |
| `error.message` | `Server overloaded; retry later.` |

Retry with exponential backoff and jitter. Do not treat overload as a terminal session failure.

## Minimal stdio client checklist

<Check>
Spawn `codex-app-server` (or `codex app-server`) with stdin/stdout piped and `CODEX_HOME` set.
</Check>

<Check>
Writer: one JSON object + `\n` per message; flush after each.
</Check>

<Check>
Reader: `read_line` loop; parse UTF-8 JSON per line.
</Check>

<Check>
Handshake: `initialize` → match response id → `initialized` notification.
</Check>

<Check>
Use a registered `clientInfo.name` for production enterprise UIs.
</Check>

<Check>
Background read loop handles server `request` messages while awaiting any client `response`.
</Check>

<Check>
Buffer unrelated messages; correlate strictly by `id`.
</Check>

<Check>
Handle `-32001` with backoff.
</Check>

## Schema generation

Generate typed bindings from the running binary so wire shapes match the server version:

```bash
codex app-server generate-ts --out DIR
codex app-server generate-json-schema --out DIR
```

## Related pages

<CardGroup>
<Card title="Protocol and transport" href="/protocol-and-transport">
JSON-RPC wire rules, transports, health probes, and backpressure.
</Card>
<Card title="Connection lifecycle" href="/connection-lifecycle">
Per-connection initialize, notification opt-out, and subscribe semantics.
</Card>
<Card title="Quickstart" href="/quickstart">
End-to-end initialize → thread/start → turn/start sequence.
</Card>
<Card title="Approvals and server requests" href="/approvals-and-server-requests">
Approval methods, MCP elicitation, and `serverRequest/resolved` lifecycle.
</Card>
<Card title="Stream turns and events" href="/stream-turns-and-events">
Consuming turn and item notifications during active work.
</Card>
</CardGroup>
