agcodex_protocol/
mcp_protocol.rs

1use std::collections::HashMap;
2use std::fmt::Display;
3use std::path::PathBuf;
4
5use crate::config_types::ReasoningEffort;
6use crate::config_types::ReasoningSummary;
7use crate::config_types::SandboxMode;
8use crate::protocol::AskForApproval;
9use crate::protocol::FileChange;
10use crate::protocol::ReviewDecision;
11use crate::protocol::SandboxPolicy;
12use crate::protocol::TurnAbortReason;
13use agcodex_mcp_types::RequestId;
14use serde::Deserialize;
15use serde::Serialize;
16use ts_rs::TS;
17use uuid::Uuid;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
20#[ts(type = "string")]
21pub struct ConversationId(pub Uuid);
22
23impl Display for ConversationId {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        write!(f, "{}", self.0)
26    }
27}
28
29#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, TS)]
30#[ts(type = "string")]
31pub struct GitSha(pub String);
32
33impl GitSha {
34    pub fn new(sha: &str) -> Self {
35        Self(sha.to_string())
36    }
37}
38
39/// Request from the client to the server.
40#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
41#[serde(tag = "method", rename_all = "camelCase")]
42pub enum ClientRequest {
43    NewConversation {
44        #[serde(rename = "id")]
45        request_id: RequestId,
46        params: NewConversationParams,
47    },
48    SendUserMessage {
49        #[serde(rename = "id")]
50        request_id: RequestId,
51        params: SendUserMessageParams,
52    },
53    SendUserTurn {
54        #[serde(rename = "id")]
55        request_id: RequestId,
56        params: SendUserTurnParams,
57    },
58    InterruptConversation {
59        #[serde(rename = "id")]
60        request_id: RequestId,
61        params: InterruptConversationParams,
62    },
63    AddConversationListener {
64        #[serde(rename = "id")]
65        request_id: RequestId,
66        params: AddConversationListenerParams,
67    },
68    RemoveConversationListener {
69        #[serde(rename = "id")]
70        request_id: RequestId,
71        params: RemoveConversationListenerParams,
72    },
73    LoginChatGpt {
74        #[serde(rename = "id")]
75        request_id: RequestId,
76    },
77    CancelLoginChatGpt {
78        #[serde(rename = "id")]
79        request_id: RequestId,
80        params: CancelLoginChatGptParams,
81    },
82    GitDiffToRemote {
83        #[serde(rename = "id")]
84        request_id: RequestId,
85        params: GitDiffToRemoteParams,
86    },
87}
88
89#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, TS)]
90#[serde(rename_all = "camelCase")]
91pub struct NewConversationParams {
92    /// Optional override for the model name (e.g. "o3", "o4-mini").
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub model: Option<String>,
95
96    /// Configuration profile from config.toml to specify default options.
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub profile: Option<String>,
99
100    /// Working directory for the session. If relative, it is resolved against
101    /// the server process's current working directory.
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub cwd: Option<String>,
104
105    /// Approval policy for shell commands generated by the model:
106    /// `untrusted`, `on-failure`, `on-request`, `never`.
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub approval_policy: Option<AskForApproval>,
109
110    /// Sandbox mode: `read-only`, `workspace-write`, or `danger-full-access`.
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub sandbox: Option<SandboxMode>,
113
114    /// Individual config settings that will override what is in
115    /// CODEX_HOME/config.toml.
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub config: Option<HashMap<String, serde_json::Value>>,
118
119    /// The set of instructions to use instead of the default ones.
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub base_instructions: Option<String>,
122
123    /// Whether to include the plan tool in the conversation.
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub include_plan_tool: Option<bool>,
126
127    /// Whether to include the apply patch tool in the conversation.
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub include_apply_patch_tool: Option<bool>,
130}
131
132#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
133#[serde(rename_all = "camelCase")]
134pub struct NewConversationResponse {
135    pub conversation_id: ConversationId,
136    pub model: String,
137}
138
139#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
140#[serde(rename_all = "camelCase")]
141pub struct AddConversationSubscriptionResponse {
142    pub subscription_id: Uuid,
143}
144
145#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
146#[serde(rename_all = "camelCase")]
147pub struct RemoveConversationSubscriptionResponse {}
148
149#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
150#[serde(rename_all = "camelCase")]
151pub struct LoginChatGptResponse {
152    pub login_id: Uuid,
153    /// URL the client should open in a browser to initiate the OAuth flow.
154    pub auth_url: String,
155}
156
157#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
158#[serde(rename_all = "camelCase")]
159pub struct GitDiffToRemoteResponse {
160    pub sha: GitSha,
161    pub diff: String,
162}
163
164// Event name for notifying client of login completion or failure.
165pub const LOGIN_CHATGPT_COMPLETE_EVENT: &str = "codex/event/login_chatgpt_complete";
166
167#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
168#[serde(rename_all = "camelCase")]
169pub struct LoginChatGptCompleteNotification {
170    pub login_id: Uuid,
171    pub success: bool,
172    #[serde(skip_serializing_if = "Option::is_none")]
173    pub error: Option<String>,
174}
175
176#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
177#[serde(rename_all = "camelCase")]
178pub struct CancelLoginChatGptParams {
179    pub login_id: Uuid,
180}
181
182#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
183#[serde(rename_all = "camelCase")]
184pub struct GitDiffToRemoteParams {
185    pub cwd: PathBuf,
186}
187
188#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
189#[serde(rename_all = "camelCase")]
190pub struct CancelLoginChatGptResponse {}
191
192#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
193#[serde(rename_all = "camelCase")]
194pub struct SendUserMessageParams {
195    pub conversation_id: ConversationId,
196    pub items: Vec<InputItem>,
197}
198
199#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
200#[serde(rename_all = "camelCase")]
201pub struct SendUserTurnParams {
202    pub conversation_id: ConversationId,
203    pub items: Vec<InputItem>,
204    pub cwd: PathBuf,
205    pub approval_policy: AskForApproval,
206    pub sandbox_policy: SandboxPolicy,
207    pub model: String,
208    pub effort: ReasoningEffort,
209    pub summary: ReasoningSummary,
210}
211
212#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
213#[serde(rename_all = "camelCase")]
214pub struct SendUserTurnResponse {}
215
216#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
217#[serde(rename_all = "camelCase")]
218pub struct InterruptConversationParams {
219    pub conversation_id: ConversationId,
220}
221
222#[derive(Serialize, Deserialize, Debug, Clone, TS)]
223#[serde(rename_all = "camelCase")]
224pub struct InterruptConversationResponse {
225    pub abort_reason: TurnAbortReason,
226}
227
228#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
229#[serde(rename_all = "camelCase")]
230pub struct SendUserMessageResponse {}
231
232#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
233#[serde(rename_all = "camelCase")]
234pub struct AddConversationListenerParams {
235    pub conversation_id: ConversationId,
236}
237
238#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
239#[serde(rename_all = "camelCase")]
240pub struct RemoveConversationListenerParams {
241    pub subscription_id: Uuid,
242}
243
244#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
245#[serde(rename_all = "camelCase")]
246#[serde(tag = "type", content = "data")]
247pub enum InputItem {
248    Text {
249        text: String,
250    },
251    /// Pre‑encoded data: URI image.
252    Image {
253        image_url: String,
254    },
255
256    /// Local image path provided by the user.  This will be converted to an
257    /// `Image` variant (base64 data URL) during request serialization.
258    LocalImage {
259        path: PathBuf,
260    },
261}
262
263// TODO(mbolin): Need test to ensure these constants match the enum variants.
264
265pub const APPLY_PATCH_APPROVAL_METHOD: &str = "applyPatchApproval";
266pub const EXEC_COMMAND_APPROVAL_METHOD: &str = "execCommandApproval";
267
268/// Request initiated from the server and sent to the client.
269#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
270#[serde(tag = "method", rename_all = "camelCase")]
271pub enum ServerRequest {
272    /// Request to approve a patch.
273    ApplyPatchApproval {
274        #[serde(rename = "id")]
275        request_id: RequestId,
276        params: ApplyPatchApprovalParams,
277    },
278    /// Request to exec a command.
279    ExecCommandApproval {
280        #[serde(rename = "id")]
281        request_id: RequestId,
282        params: ExecCommandApprovalParams,
283    },
284}
285
286#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
287pub struct ApplyPatchApprovalParams {
288    pub conversation_id: ConversationId,
289    /// Use to correlate this with [codex_core::protocol::PatchApplyBeginEvent]
290    /// and [codex_core::protocol::PatchApplyEndEvent].
291    pub call_id: String,
292    pub file_changes: HashMap<PathBuf, FileChange>,
293    /// Optional explanatory reason (e.g. request for extra write access).
294    #[serde(skip_serializing_if = "Option::is_none")]
295    pub reason: Option<String>,
296    /// When set, the agent is asking the user to allow writes under this root
297    /// for the remainder of the session (unclear if this is honored today).
298    #[serde(skip_serializing_if = "Option::is_none")]
299    pub grant_root: Option<PathBuf>,
300}
301
302#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
303pub struct ExecCommandApprovalParams {
304    pub conversation_id: ConversationId,
305    /// Use to correlate this with [codex_core::protocol::ExecCommandBeginEvent]
306    /// and [codex_core::protocol::ExecCommandEndEvent].
307    pub call_id: String,
308    pub command: Vec<String>,
309    pub cwd: PathBuf,
310    #[serde(skip_serializing_if = "Option::is_none")]
311    pub reason: Option<String>,
312}
313
314#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
315pub struct ExecCommandApprovalResponse {
316    pub decision: ReviewDecision,
317}
318
319#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
320pub struct ApplyPatchApprovalResponse {
321    pub decision: ReviewDecision,
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327    use pretty_assertions::assert_eq;
328    use serde_json::json;
329
330    #[test]
331    fn serialize_new_conversation() {
332        let request = ClientRequest::NewConversation {
333            request_id: RequestId::Integer(42),
334            params: NewConversationParams {
335                model: Some("gpt-5".to_string()),
336                profile: None,
337                cwd: None,
338                approval_policy: Some(AskForApproval::OnRequest),
339                sandbox: None,
340                config: None,
341                base_instructions: None,
342                include_plan_tool: None,
343                include_apply_patch_tool: None,
344            },
345        };
346        assert_eq!(
347            json!({
348                "method": "newConversation",
349                "id": 42,
350                "params": {
351                    "model": "gpt-5",
352                    "approvalPolicy": "on-request"
353                }
354            }),
355            serde_json::to_value(&request).unwrap(),
356        );
357    }
358}