Skip to main content

codex/mcp/
protocol.rs

1use std::{collections::BTreeMap, ffi::OsString, path::PathBuf, time::Duration};
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use tokio::sync::{mpsc, oneshot};
6
7/// JSON-RPC method name used to initialize MCP servers.
8pub const METHOD_INITIALIZE: &str = "initialize";
9/// JSON-RPC method name used to shut down MCP servers.
10pub const METHOD_SHUTDOWN: &str = "shutdown";
11/// JSON-RPC method name used after shutdown to signal exit.
12pub const METHOD_EXIT: &str = "exit";
13/// JSON-RPC cancellation method per the spec.
14pub const METHOD_CANCEL: &str = "$/cancelRequest";
15
16/// Tool-call method exposed by the MCP server.
17pub const METHOD_CODEX: &str = "tools/call";
18/// Tool-call method for follow-up prompts (codex-reply).
19pub const METHOD_CODEX_REPLY: &str = "tools/call";
20/// Notification channel emitted by `codex mcp-server`.
21pub const METHOD_CODEX_EVENT: &str = "codex/event";
22/// Expected approval response hook (server-specific; confirmed during E2).
23pub const METHOD_CODEX_APPROVAL: &str = "codex/approval";
24
25/// Method names exposed by `codex app-server`.
26pub const METHOD_THREAD_START: &str = "thread/start";
27/// Resume an existing thread.
28pub const METHOD_THREAD_RESUME: &str = "thread/resume";
29/// List threads (paged).
30pub const METHOD_THREAD_LIST: &str = "thread/list";
31/// Fork an existing thread.
32pub const METHOD_THREAD_FORK: &str = "thread/fork";
33/// Start a new turn on a thread.
34pub const METHOD_TURN_START: &str = "turn/start";
35/// Interrupt an active turn.
36pub const METHOD_TURN_INTERRUPT: &str = "turn/interrupt";
37
38/// Unique identifier for JSON-RPC calls.
39pub type RequestId = u64;
40
41/// Stream of notifications surfaced alongside a JSON-RPC response.
42pub type EventStream<T> = mpsc::UnboundedReceiver<T>;
43
44/// Shared launch configuration for stdio MCP/app-server processes.
45///
46/// The Workstream A env-prep helper should populate `binary`, `code_home`, and
47/// baseline environment entries. Callers can layer additional `env` entries for
48/// per-call overrides (e.g., `RUST_LOG`). `mirror_stdio` controls whether raw
49/// stdout/stderr should be mirrored to the host console in addition to being
50/// parsed as JSON-RPC.
51#[derive(Clone, Debug)]
52pub struct StdioServerConfig {
53    pub binary: PathBuf,
54    pub code_home: Option<PathBuf>,
55    pub current_dir: Option<PathBuf>,
56    pub env: Vec<(OsString, OsString)>,
57    /// Enables the `codex app-server --analytics-default-enabled` flag when launching app-server.
58    pub app_server_analytics_default_enabled: bool,
59    pub mirror_stdio: bool,
60    pub startup_timeout: Duration,
61}
62
63/// Client metadata attached to the `initialize` request.
64#[derive(Clone, Debug, Serialize, Deserialize)]
65pub struct ClientInfo {
66    pub name: String,
67    pub version: String,
68}
69
70/// Parameters for the initial `initialize` handshake.
71#[derive(Clone, Debug, Serialize, Deserialize)]
72pub struct InitializeParams {
73    #[serde(rename = "clientInfo")]
74    pub client: ClientInfo,
75    #[serde(rename = "protocolVersion")]
76    pub protocol_version: String,
77    #[serde(default)]
78    pub capabilities: Value,
79}
80
81/// Parameters for `codex/codex` (new session).
82#[derive(Clone, Debug, Serialize, Deserialize)]
83#[serde(rename_all = "kebab-case")]
84pub struct CodexCallParams {
85    pub prompt: String,
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub model: Option<String>,
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub cwd: Option<PathBuf>,
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub sandbox: Option<String>,
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub approval_policy: Option<String>,
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub profile: Option<String>,
96    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
97    pub config: BTreeMap<String, Value>,
98}
99
100/// Parameters for `codex/codex-reply` (continue an existing conversation).
101#[derive(Clone, Debug, Serialize, Deserialize)]
102pub struct CodexReplyParams {
103    #[serde(rename = "conversationId")]
104    pub conversation_id: String,
105    pub prompt: String,
106}
107
108/// Classification for approval prompts surfaced by the MCP server.
109#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
110pub enum ApprovalKind {
111    Exec,
112    Apply,
113    Unknown(String),
114}
115
116/// Approval request emitted as part of a `codex/event` notification.
117#[derive(Clone, Debug, Serialize, Deserialize)]
118pub struct ApprovalRequest {
119    pub approval_id: String,
120    pub kind: ApprovalKind,
121    /// Full payload from the server so callers can render UI or inspect diffs/commands.
122    pub payload: Value,
123}
124
125/// Decision payload sent back to the MCP server in response to an approval prompt.
126#[derive(Clone, Debug, Serialize, Deserialize)]
127pub enum ApprovalDecision {
128    Approve {
129        approval_id: String,
130    },
131    Reject {
132        approval_id: String,
133        reason: Option<String>,
134    },
135}
136
137/// Notification emitted by `codex/event`.
138#[derive(Clone, Debug, Serialize, Deserialize)]
139#[serde(tag = "type", rename_all = "snake_case")]
140pub enum CodexEvent {
141    TaskComplete {
142        conversation_id: String,
143        result: Value,
144    },
145    ApprovalRequired(ApprovalRequest),
146    Cancelled {
147        conversation_id: Option<String>,
148        reason: Option<String>,
149    },
150    Error {
151        message: String,
152        data: Option<Value>,
153    },
154    Raw {
155        method: String,
156        params: Value,
157    },
158}
159
160/// Final response payload for `codex/codex` or `codex/codex-reply`.
161#[derive(Clone, Debug, Serialize, Deserialize)]
162pub struct CodexCallResult {
163    #[serde(default, rename = "conversationId", alias = "conversation_id")]
164    pub conversation_id: Option<String>,
165    #[serde(default, rename = "content", alias = "output")]
166    pub output: Value,
167}
168
169/// Handle returned for each codex call, bundling response and notifications.
170pub struct CodexCallHandle {
171    pub request_id: RequestId,
172    pub events: EventStream<CodexEvent>,
173    pub response: oneshot::Receiver<Result<CodexCallResult, super::McpError>>,
174}
175
176/// Parameters for `thread/start`.
177#[derive(Clone, Debug, Serialize, Deserialize)]
178pub struct ThreadStartParams {
179    pub thread_id: Option<String>,
180    #[serde(default)]
181    pub metadata: Value,
182}
183
184/// Parameters for `thread/resume`.
185#[derive(Clone, Debug, Serialize, Deserialize)]
186#[serde(rename_all = "camelCase")]
187pub struct ThreadResumeParams {
188    pub thread_id: String,
189}
190
191/// Sorting key for `thread/list`.
192#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
193#[serde(rename_all = "snake_case")]
194pub enum ThreadListSortKey {
195    CreatedAt,
196    UpdatedAt,
197}
198
199/// Parameters for `thread/list`.
200#[derive(Clone, Debug, Serialize, Deserialize)]
201#[serde(rename_all = "camelCase")]
202pub struct ThreadListParams {
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub cwd: Option<PathBuf>,
205    /// Opaque pagination cursor; MUST be explicitly `null` on the first request.
206    pub cursor: Option<String>,
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub limit: Option<u32>,
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub sort_key: Option<ThreadListSortKey>,
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub archived: Option<bool>,
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub model_providers: Option<Vec<String>>,
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub source_kinds: Option<Vec<String>>,
217}
218
219/// Response payload for `thread/list`.
220#[derive(Clone, Debug, Serialize, Deserialize)]
221#[serde(rename_all = "camelCase")]
222pub struct ThreadListResponse {
223    pub data: Vec<ThreadSummary>,
224    pub next_cursor: Option<String>,
225}
226
227/// Thread metadata summary returned by `thread/list`.
228#[derive(Clone, Debug, Serialize, Deserialize)]
229#[serde(rename_all = "camelCase")]
230pub struct ThreadSummary {
231    pub id: String,
232    pub created_at: i64,
233    pub updated_at: i64,
234    #[serde(default, skip_serializing_if = "Option::is_none")]
235    pub cwd: Option<PathBuf>,
236    #[serde(default, skip_serializing_if = "BTreeMap::is_empty", flatten)]
237    pub extra: BTreeMap<String, Value>,
238}
239
240/// Parameters for `thread/fork`.
241#[derive(Clone, Debug, Serialize, Deserialize)]
242#[serde(rename_all = "camelCase")]
243pub struct ThreadForkParams {
244    pub thread_id: String,
245    #[serde(skip_serializing_if = "Option::is_none")]
246    pub cwd: Option<PathBuf>,
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub approval_policy: Option<String>,
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub sandbox: Option<String>,
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub persist_extended_history: Option<bool>,
253}
254
255/// Response payload for `thread/fork`.
256#[derive(Clone, Debug, Serialize, Deserialize)]
257pub struct ThreadForkResponse {
258    pub thread: ForkedThread,
259}
260
261/// Forked thread metadata returned by `thread/fork`.
262#[derive(Clone, Debug, Serialize, Deserialize)]
263pub struct ForkedThread {
264    pub id: String,
265}
266
267/// Parameters for `turn/start`.
268#[derive(Clone, Debug, Serialize, Deserialize)]
269#[serde(rename_all = "camelCase")]
270pub struct TurnStartParams {
271    pub thread_id: String,
272    #[serde(rename = "input", alias = "prompt")]
273    pub input: Vec<TurnInput>,
274    pub model: Option<String>,
275    #[serde(default)]
276    pub config: BTreeMap<String, Value>,
277}
278
279#[derive(Clone, Debug, Serialize, Deserialize)]
280pub struct TurnInput {
281    #[serde(rename = "type")]
282    pub kind: String,
283    #[serde(skip_serializing_if = "Option::is_none")]
284    pub text: Option<String>,
285}
286
287/// Parameters for `turn/start` (fork-focused pinned v2 subset).
288#[derive(Clone, Debug, Serialize, Deserialize)]
289#[serde(rename_all = "camelCase")]
290pub struct TurnStartParamsV2 {
291    pub thread_id: String,
292    pub input: Vec<UserInputV2>,
293    #[serde(skip_serializing_if = "Option::is_none")]
294    pub approval_policy: Option<String>,
295    #[serde(skip_serializing_if = "Option::is_none")]
296    pub cwd: Option<PathBuf>,
297}
298
299/// User input shape pinned for fork flows.
300#[derive(Clone, Debug, Serialize, Deserialize)]
301#[serde(tag = "type", rename_all = "snake_case")]
302pub enum UserInputV2 {
303    Text {
304        text: String,
305        #[serde(default)]
306        text_elements: Vec<Value>,
307    },
308}
309
310impl UserInputV2 {
311    pub fn text(text: impl Into<String>) -> Self {
312        Self::Text {
313            text: text.into(),
314            text_elements: Vec::new(),
315        }
316    }
317}
318
319/// Parameters for `turn/interrupt`.
320#[derive(Clone, Debug, Serialize, Deserialize)]
321#[serde(rename_all = "camelCase")]
322pub struct TurnInterruptParams {
323    pub thread_id: Option<String>,
324    pub turn_id: String,
325}
326
327/// Notification emitted by the app-server.
328#[derive(Clone, Debug, Serialize, Deserialize)]
329#[serde(tag = "type", rename_all = "snake_case")]
330pub enum AppNotification {
331    TaskComplete {
332        thread_id: String,
333        turn_id: Option<String>,
334        result: Value,
335    },
336    Item {
337        thread_id: String,
338        turn_id: Option<String>,
339        item: Value,
340    },
341    Error {
342        message: String,
343        data: Option<Value>,
344    },
345    Raw {
346        method: String,
347        params: Value,
348    },
349}
350
351/// Handle returned for each app-server call, bundling response and notifications.
352pub struct AppCallHandle {
353    pub request_id: RequestId,
354    pub events: EventStream<AppNotification>,
355    pub response: oneshot::Receiver<Result<Value, super::McpError>>,
356}