Codex App Serverは、自分で作った画面(独自UI)からCodexの会話を動かすための接続口です。codex app-serverを起動し、接続ごとにinitializeとinitializedを送ってから、thread/startで会話を作り、turn/startで依頼を始めます。その後に届く通知を読み続け、完了した項目・承認の依頼・ターンの結果を画面へ反映します。ただし、app-serverのコマンドは実験的な機能で、公式資料は本番ワークロードをサポート対象外としています。
この記事は、OpenAIの公式資料(2026年10月29日に確認)と、編集部の環境に入っているCodex CLI 0.165.0で生成したスキーマ定義との照合に基づく解説です。スキーマの生成はローカルで完結する処理で、認証やモデルの呼び出しは行っていません。下のコードは考え方を学ぶための教材です。Codex本体に依頼を送る実測は行っていません。
Codex App Serverを使う場面:SDKや非対話実行との違い
公式資料によると、App ServerはCodexのVS Code拡張のような高機能なクライアントを支えている仕組みです。認証、会話の履歴、承認、作業中のイベント配信まで自分の製品に深く組み込みたいときに使います。反対に、ジョブの自動化やCIでCodexを動かしたいだけなら、公式はCodex SDKを勧めています。
| やりたいこと | 向いている入口 | 理由 |
|---|---|---|
| 自社の画面で会話・途中経過・承認ボタンを出したい | Codex App Server | 会話、ターン、項目ごとの通知と承認の要求を、クライアント側で受け取れる |
| アプリのコードからCodexに作業を頼み、結果を受け取りたい | Codex SDK | 会話の開始・継続をライブラリの関数で扱え、低い層の通知を自分で組み立てずに済む |
| スクリプトやCIで一回の作業を実行したい | codex exec | 最終結果を標準出力へ出す非対話の実行に向く |
| Codexから社内ツールやデータを使いたい | 外部MCPサーバーの接続 | Codexが呼び出す側になる。App Serverとは向きが逆 |
注意したいのは、以前あったcodex mcp-serverコマンドと単体のcodex-mcp-serverが削除されたことです。公式の移行案内では、Codexを別のアプリから操作していた連携はApp Serverへ移すよう求めています。App ServerはMCPサーバーではなく、MCPのツール呼び出しをそのまま置き換えるものでもありません。独自のJSON-RPCの手順に書き直す必要があります。Codexから外部のMCPサーバーを使う設定は引き続き使えるので、その方法はCodexのMCP設定を参照してください。
Codex App Serverを使う前に確かめる3つの条件
実装に入る前に、使ってよい範囲を決めておきます。ここを曖昧にしたまま作り込むと、後から作り直しになりやすいからです。
実験的な機能で、本番ワークロードはサポート対象外
公式資料は、app-serverのコマンドとWebSocketでの接続を「実験的で、本番ワークロードはサポートしない」と明記しています。編集部としては、社内の試作や手元の開発者向けツールで仕組みを確かめる範囲にとどめ、顧客向けの本番機能として約束しないことを勧めます。仕様は版ごとに変わる前提で、更新のたびに動作を確認する計画を持っておきます。
認証の仕組みを、商用・ホスト型のサービスに使わない
App Serverにはログイン状態の確認やログインを始める手順もあります。ただし公式資料は、App Serverの認証を商用サービスやホスト型サービスに使うことは一度も認めていないと書いています。そうした用途にはSign in with ChatGPTを使うよう案内しています。ローカルやオープンソースのアプリで既に使っている場合は使い続けられますが、公式はSign in with ChatGPTへの移行を勧めています。個人や社内のローカルなツールと、利用者にサービスとして提供する製品とでは、認証の選び方が違うことを最初に整理しておきましょう。
接続はstdioから始める
接続方法は、標準入出力を使うstdioが既定です。1行に1つのJSONを書く形式(JSONL)で、手元のプロセス同士をつなぐのに向いています。WebSocketでの接続も用意されていますが、実験的でサポート対象外です。ローカル以外から受け付ける場合は、公式資料にある認証の設定を先に行い、TLSで暗号化した接続の後ろに置く必要があります。最初の試作では、外部へ待ち受けを開かないstdioで十分です。

App Serverを起動して接続を初期化する手順
接続して最初にやることは、クライアントの名乗りです。この手順を飛ばすと、ほかの操作はすべて断られます。
- ターミナルで
codex app-serverを起動する。引数を付けなければstdioで待ち受けます。 - 接続したら、
initializeの要求を1回だけ送り、clientInfoに自分のクライアント名・表示名・版を入れる。 - 応答を受け取ったら、
initializedの通知を送る。 - ここで初めて、会話の作成などの操作を送れるようになる。
{"method":"initialize","id":0,"params":{"clientInfo":{"name":"my_product","title":"My Product","version":"0.1.0"}}}
{"method":"initialized","params":{}}
{"method":"thread/start","id":1,"params":{"cwd":"/Users/me/project"}}
初期化の前に別の操作を送るとNot initialized、同じ接続でinitializeを2回送るとAlready initializedというエラーになります。接続が切れて張り直した場合は、新しい接続でもう一度初期化から始めます。clientInfo.nameは、企業向けの監査ログ(Compliance Logs Platform)でクライアントを見分けるのにも使われるので、試作の段階から自分の製品だと分かる名前を付けておきましょう。
一部のメソッドや項目は実験的な扱いで、initializeのcapabilitiesに"experimentalApi": trueを入れた接続でしか使えません。指定しないまま実験的な項目を送ると、requires experimentalApi capabilityというエラーで拒否されます。安定版の範囲だけで作るのか、実験的な項目も使うのかを、接続の設計で決めておきます。
型定義は、手で書き写すより生成するほうが安全です。codex app-server generate-ts --out ./schemasでTypeScriptの型を、generate-json-schemaでJSON Schemaを出力できます。生成物はそのとき使ったCodexの版に合わせて作られるため、Codexを更新したら型も作り直します。
App Serverで会話(thread)を作り、依頼(turn)を始める
App Serverの中心になる言葉は3つです。threadは利用者とCodexの一続きの会話、turnは1回の依頼とそれに続く作業、itemはその中で起きる一つひとつの出来事(利用者の発言、Codexの返答、コマンドの実行、ファイルの変更など)を指します。
thread/startには、作業するフォルダー(cwd)、承認の方針(approvalPolicy)、作業の権限(サンドボックス、sandbox)などを指定できます。応答に含まれるthread.idが、この会話を指すIDです。次にturn/startで、そのthreadIdと依頼文を送ります。
{"method":"turn/start","id":2,"params":{"threadId":"thr_123","input":[{"type":"text","text":"テストを実行し、失敗があれば原因を説明してください"}]}}
ターンごとに、モデル、推論の深さ、作業フォルダー、サンドボックスの設定などを上書きできます。公式資料によると、上書きした設定はそのthreadの以後のターンの既定値になります。一方、結果の形を決めるoutputSchemaは、そのターンだけに効きます。「次の依頼では元の設定に戻るはず」と思い込まず、画面側でも現在の設定を表示しておくと、利用者が混乱しにくくなります。
threadIdはクライアント側で保存する
会話を後で続けるには、thread.idを自分のアプリ側に保存しておき、thread/resumeに渡します。公式資料は、会話の系統をまとめるIDはthread.sessionIdから読むよう求めており、threadIdから推測しないよう注意しています。分岐(thread/fork)した会話は、元の会話のsessionIdを引き継ぐためです。
App Serverの通知を画面へ反映する考え方
ターンを始めると、App Serverから通知が順に届きます。画面に何をどの時点で出すかは、通知の役割で決めます。
| 通知 | 意味 | 画面での扱いの例 |
|---|---|---|
turn/started | ターンが始まった | 入力欄を「作業中」にし、中断ボタンを出す |
item/started | 新しい作業の単位が始まった | 返答やコマンドの枠を作る |
item/agentMessage/delta | 返答の文字が少しずつ届く | 枠に追記して途中経過を見せる |
item/completed | その単位の最終状態 | 途中の表示をこの内容で置き換える |
turn/diff/updated | このターンのファイル変更をまとめた差分 | 差分の欄を更新する |
turn/completed | ターンの終了(完了・中断・失敗) | 状態を確定し、入力欄を戻す |
公式資料は、item/completedを最終的な状態として扱うよう書いています。計画(plan)の項目については、途中の断片をつなげた文字列と最終の内容が一致しないことがあるとも説明されています。画面は完了の通知で上書きしましょう。また公式資料は、項目の一覧はturn/diff/updatedやturn/plan/updatedからではなく、item/*の通知を正として組み立てるよう求めています。
次のコードは、届いた1行ずつのJSONを画面用の状態へ反映する編集部の教材です。完了した項目を正とし、承認の依頼は解決の通知が届くまで開いたままにします。扱うのはコマンドとファイル変更の承認だけで、利用者への質問(item/tool/requestUserInput)やMCPサーバーからの入力依頼などの要求は省いています。
// codex-ui-state.mjs(Codex Studio編集部の教材)
export function createState() {
return { threadId: null, turns: new Map(), items: new Map(), approvals: new Map(), errors: [] };
}
export function apply(state, msg) {
// idとmethodの両方がある行は、サーバーからの要求(承認など)
if (msg.id !== undefined && msg.method) {
if (msg.method.endsWith("/requestApproval")) {
const { itemId, turnId } = msg.params;
state.approvals.set(msg.id, { method: msg.method, itemId, turnId });
}
return state;
}
const p = msg.params ?? {};
switch (msg.method) {
case "thread/started":
state.threadId ??= p.thread.id; // UI側で保存し、再接続に使う
break;
case "turn/started":
state.turns.set(p.turn.id, { status: p.turn.status });
break;
case "item/started":
state.items.set(p.item.id, { ...p.item, final: false });
break;
case "item/agentMessage/delta": {
const item = state.items.get(p.itemId);
if (item && !item.final) item.text = (item.text ?? "") + p.delta; // 途中表示だけ
break;
}
case "item/completed":
state.items.set(p.item.id, { ...p.item, final: true }); // 完了時の内容を正とする
break;
case "serverRequest/resolved":
state.approvals.delete(p.requestId); // 回答済み・取り消し済みの承認を閉じる
break;
case "error":
state.errors.push({ turnId: p.turnId, message: p.error.message, willRetry: p.willRetry });
break;
case "turn/completed":
state.turns.set(p.turn.id, { status: p.turn.status, error: p.turn.error ?? null });
break;
}
return state;
}
編集部では、公式資料のメッセージ例に沿って作った12行の架空の通知列をNode.jsでこの教材に通しました。返答の途中表示が「テストを」「テストを実行します」と伸び、承認待ちは依頼の時点で1件、解決の通知で0件に戻り、完了後に遅れて届いた断片は表示に混ざりませんでした。これは教材コードの検査であり、App Serverが実際にこの順番で通知を送ることを測ったものではありません。
App Serverの承認依頼に答える流れ
利用者の設定によっては、コマンドの実行やファイルの変更の前に承認が必要になります。このとき、App Serverからクライアントへ「要求」が届きます。通知と違ってidが付いているので、クライアントは同じidで答えを返します。
item/startedで、保留中のコマンド(またはファイル変更)の項目が届く。item/commandExecution/requestApproval(ファイル変更ならitem/fileChange/requestApproval)が、threadId・turnId・itemIdを付けて届く。- クライアントが利用者の判断を返す。
serverRequest/resolvedで、その依頼が答え済みまたは取り消し済みになったと知らされる。item/completedで、実行済み・失敗・却下のいずれかの最終状態が届く。
{"id":60,"result":{"decision":"decline"}}
選べる答えは、許可(accept)、このセッション中は確認を省く(acceptForSession)、却下(decline)、取り消し(cancel)が基本です。編集部が生成したスキーマの説明では、acceptForSessionで確認が省かれるのは、ファイル変更なら同じファイルへのその後の変更、コマンドならこのセッションの承認キャッシュ(一度許可した内容の記録)に当たるその後の確認に限られます。ボタンの文言も「このファイルへの変更は、このセッション中は確認しない」のように対象を示すと、許可の範囲を誤解されにくくなります。編集部が生成したスキーマの説明では、ファイル変更をdeclineするとCodexはそのターンを続け、cancelするとターンもすぐに中断されます。画面のボタン名を「拒否」だけにすると、どちらの動きになるのか利用者に伝わりません。「この変更だけ断る」と「作業を止める」を分けて表示しましょう。
依頼に付いているthreadIdとturnIdは、承認ボタンをどの会話のどの依頼に出すかを決めるのに使います。複数の会話を同時に開く画面では特に重要です。また、networkApprovalContextが付く承認は一般のコマンドの承認ではなく、管理されたネットワーク接続の承認です。公式資料はネットワーク用の確認を出すよう求めており、スキーマはその接続先のホストとプロトコルを示しています。
中断・再接続・混雑への備え方
作業を途中で止めるには、turn/interruptにthreadIdとturnIdを送ります。成功すると、そのターンはinterruptedの状態で終わります。中断ではなく方向を変えたいだけなら、turn/steerで実行中のターンに追加の指示を送れます。このときexpectedTurnIdに今のターンの番号を入れ、合わなければ要求は失敗します。古い画面から別のターンへ指示が紛れ込むのを防ぐのに役立ちます。
接続が切れた後は、次の順で戻すと状態を取り違えにくくなります。
- 新しい接続で
initializeとinitializedを送り直す。 - 保存しておいたthreadIdを
thread/resumeに渡す。新しく作るローカルの会話は履歴をページに分けて持つのが既定で、公式資料はその場合に履歴全体を返す形を非推奨としています。excludeTurns: trueを付けて、会話の情報だけを受け取ります。 - 必要な履歴は
thread/turns/listとthread/items/listでページごとに読み込む。 - 切断中に終わったかもしれない承認は、画面に残したままにしない。再接続後に保留中の承認が送り直されるかは公式資料に書かれていないため、編集部としては、改めて届いた要求だけを表示する作りを勧めます。
ターンが失敗すると、App Serverはerrorを送ったあと、failedの状態でターンを終えます。理由はcodexErrorInfoに入り、文脈の上限超過(ContextWindowExceeded)、利用上限(UsageLimitExceeded)、認証エラー(Unauthorized)などが公式資料に挙がっています。編集部が生成したスキーマでは、errorの通知にwillRetry(真偽値)という項目もありました。ただし説明文はなく公式資料にも記載がないため、名前からは再試行の予定を示すようにも読めますが、意味は確認できていません。失敗を確定させるのは、turn/completedの状態がfailedになったときにすると安全です。
WebSocketで接続している場合、受付の待ち行列がいっぱいになると、App Serverは新しい要求をエラー番号-32001と「Server overloaded; retry later.」で断ります。公式資料は、待ち時間を指数関数的に(たとえば1秒、2秒、4秒…と倍々に)延ばし、ランダムなばらつき(ジッター)を加えて再試行するよう勧めています。すぐに同じ要求を送り直し続けると、混雑を長引かせるだけです。
App Server連携を試作から先へ進める前の点検表
試作ができたら、次の項目を一つずつ確かめます。どれかが「分からない」のままなら、利用範囲を広げる前に設計を見直します。
- 利用範囲を、公式がサポート対象外とする本番ワークロードに広げず、社内の試作や開発者向けツールにとどめているか(この範囲は編集部の推奨)。
- 商用・ホスト型のサービスにApp Serverの認証を使っていないか。
- 接続ごとに初期化してから操作を送り、
clientInfo.nameで自分のクライアントだと分かるか。 - threadIdとsessionIdを保存し、再接続後に
thread/resumeで同じ会話へ戻れるか。 - 途中の表示を
item/completedで上書きし、ターンの終了をturn/completedで確定しているか。 - 承認の依頼に同じidで答え、「この変更だけ断る」と「作業を止める」、確認を省く対象を区別しているか。
- Codexを更新したとき、型定義を作り直して動作を確かめる手順があるか。
アプリのコードから作業を呼び出すだけなら、App Serverの細かな通知を自分で扱う必要はありません。その場合はCodex SDKの使い方を、スクリプトやCIで一回ずつ動かすならcodex execの使い方を参考にしてください。
まとめ:App Serverは画面の状態管理から設計する
Codex App Serverを使うときは、まず「実験的な機能を、どこまでの範囲で使うか」を決めます。そのうえで、初期化、会話と依頼の開始、通知の反映、承認への応答、再接続の順に、クライアント側の状態の持ち方を設計します。届いた文字を並べるだけでなく、何を正とし、何を保存し、何を閉じるかを決めておくことが、独自UIでCodexを安心して使うための近道です。
Codexを自社の開発の流れにどう組み込むか、試作の範囲や確認の手順から一緒に整理したい場合は、法人向けのご相談も受け付けています。





