This PR adds the following: * A getAuthStatus method on the mcp server. This returns the auth method currently in use (chatgpt or apikey) or none if the user is not authenticated. It also returns the "preferred auth method" which reflects the `preferred_auth_method` value in the config. * A logout method on the mcp server. If called, it logs out the user and deletes the `auth.json` file — the same behavior in the cli's `/logout` command. * An `authStatusChange` event notification that is sent when the auth status changes due to successful login or logout operations. * Logic to pass command-line config overrides to the mcp server at startup time. This allows use cases like `codex mcp -c preferred_auth_method=apikey`.
415 lines
13 KiB
Rust
415 lines
13 KiB
Rust
use std::collections::HashMap;
|
||
use std::fmt::Display;
|
||
use std::path::PathBuf;
|
||
|
||
use crate::config_types::ReasoningEffort;
|
||
use crate::config_types::ReasoningSummary;
|
||
use crate::config_types::SandboxMode;
|
||
use crate::protocol::AskForApproval;
|
||
use crate::protocol::FileChange;
|
||
use crate::protocol::ReviewDecision;
|
||
use crate::protocol::SandboxPolicy;
|
||
use crate::protocol::TurnAbortReason;
|
||
use mcp_types::RequestId;
|
||
use serde::Deserialize;
|
||
use serde::Serialize;
|
||
use strum_macros::Display;
|
||
use ts_rs::TS;
|
||
use uuid::Uuid;
|
||
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
|
||
#[ts(type = "string")]
|
||
pub struct ConversationId(pub Uuid);
|
||
|
||
impl Display for ConversationId {
|
||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
write!(f, "{}", self.0)
|
||
}
|
||
}
|
||
|
||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, TS)]
|
||
#[ts(type = "string")]
|
||
pub struct GitSha(pub String);
|
||
|
||
impl GitSha {
|
||
pub fn new(sha: &str) -> Self {
|
||
Self(sha.to_string())
|
||
}
|
||
}
|
||
|
||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, TS)]
|
||
#[serde(rename_all = "lowercase")]
|
||
pub enum AuthMode {
|
||
ApiKey,
|
||
ChatGPT,
|
||
}
|
||
|
||
/// Request from the client to the server.
|
||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||
#[serde(tag = "method", rename_all = "camelCase")]
|
||
pub enum ClientRequest {
|
||
NewConversation {
|
||
#[serde(rename = "id")]
|
||
request_id: RequestId,
|
||
params: NewConversationParams,
|
||
},
|
||
SendUserMessage {
|
||
#[serde(rename = "id")]
|
||
request_id: RequestId,
|
||
params: SendUserMessageParams,
|
||
},
|
||
SendUserTurn {
|
||
#[serde(rename = "id")]
|
||
request_id: RequestId,
|
||
params: SendUserTurnParams,
|
||
},
|
||
InterruptConversation {
|
||
#[serde(rename = "id")]
|
||
request_id: RequestId,
|
||
params: InterruptConversationParams,
|
||
},
|
||
AddConversationListener {
|
||
#[serde(rename = "id")]
|
||
request_id: RequestId,
|
||
params: AddConversationListenerParams,
|
||
},
|
||
RemoveConversationListener {
|
||
#[serde(rename = "id")]
|
||
request_id: RequestId,
|
||
params: RemoveConversationListenerParams,
|
||
},
|
||
LoginChatGpt {
|
||
#[serde(rename = "id")]
|
||
request_id: RequestId,
|
||
},
|
||
CancelLoginChatGpt {
|
||
#[serde(rename = "id")]
|
||
request_id: RequestId,
|
||
params: CancelLoginChatGptParams,
|
||
},
|
||
LogoutChatGpt {
|
||
#[serde(rename = "id")]
|
||
request_id: RequestId,
|
||
},
|
||
GetAuthStatus {
|
||
#[serde(rename = "id")]
|
||
request_id: RequestId,
|
||
},
|
||
GitDiffToRemote {
|
||
#[serde(rename = "id")]
|
||
request_id: RequestId,
|
||
params: GitDiffToRemoteParams,
|
||
},
|
||
}
|
||
|
||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, TS)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct NewConversationParams {
|
||
/// Optional override for the model name (e.g. "o3", "o4-mini").
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub model: Option<String>,
|
||
|
||
/// Configuration profile from config.toml to specify default options.
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub profile: Option<String>,
|
||
|
||
/// Working directory for the session. If relative, it is resolved against
|
||
/// the server process's current working directory.
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub cwd: Option<String>,
|
||
|
||
/// Approval policy for shell commands generated by the model:
|
||
/// `untrusted`, `on-failure`, `on-request`, `never`.
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub approval_policy: Option<AskForApproval>,
|
||
|
||
/// Sandbox mode: `read-only`, `workspace-write`, or `danger-full-access`.
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub sandbox: Option<SandboxMode>,
|
||
|
||
/// Individual config settings that will override what is in
|
||
/// CODEX_HOME/config.toml.
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub config: Option<HashMap<String, serde_json::Value>>,
|
||
|
||
/// The set of instructions to use instead of the default ones.
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub base_instructions: Option<String>,
|
||
|
||
/// Whether to include the plan tool in the conversation.
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub include_plan_tool: Option<bool>,
|
||
|
||
/// Whether to include the apply patch tool in the conversation.
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub include_apply_patch_tool: Option<bool>,
|
||
}
|
||
|
||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct NewConversationResponse {
|
||
pub conversation_id: ConversationId,
|
||
pub model: String,
|
||
}
|
||
|
||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct AddConversationSubscriptionResponse {
|
||
pub subscription_id: Uuid,
|
||
}
|
||
|
||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct RemoveConversationSubscriptionResponse {}
|
||
|
||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct LoginChatGptResponse {
|
||
pub login_id: Uuid,
|
||
/// URL the client should open in a browser to initiate the OAuth flow.
|
||
pub auth_url: String,
|
||
}
|
||
|
||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct GitDiffToRemoteResponse {
|
||
pub sha: GitSha,
|
||
pub diff: String,
|
||
}
|
||
|
||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct CancelLoginChatGptParams {
|
||
pub login_id: Uuid,
|
||
}
|
||
|
||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct GitDiffToRemoteParams {
|
||
pub cwd: PathBuf,
|
||
}
|
||
|
||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct CancelLoginChatGptResponse {}
|
||
|
||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct LogoutChatGptParams {
|
||
pub login_id: Uuid,
|
||
}
|
||
|
||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct LogoutChatGptResponse {}
|
||
|
||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct GetAuthStatusParams {
|
||
pub login_id: Uuid,
|
||
}
|
||
|
||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct GetAuthStatusResponse {
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub auth_method: Option<AuthMode>,
|
||
pub preferred_auth_method: AuthMode,
|
||
}
|
||
|
||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct SendUserMessageParams {
|
||
pub conversation_id: ConversationId,
|
||
pub items: Vec<InputItem>,
|
||
}
|
||
|
||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct SendUserTurnParams {
|
||
pub conversation_id: ConversationId,
|
||
pub items: Vec<InputItem>,
|
||
pub cwd: PathBuf,
|
||
pub approval_policy: AskForApproval,
|
||
pub sandbox_policy: SandboxPolicy,
|
||
pub model: String,
|
||
pub effort: ReasoningEffort,
|
||
pub summary: ReasoningSummary,
|
||
}
|
||
|
||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct SendUserTurnResponse {}
|
||
|
||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct InterruptConversationParams {
|
||
pub conversation_id: ConversationId,
|
||
}
|
||
|
||
#[derive(Serialize, Deserialize, Debug, Clone, TS)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct InterruptConversationResponse {
|
||
pub abort_reason: TurnAbortReason,
|
||
}
|
||
|
||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct SendUserMessageResponse {}
|
||
|
||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct AddConversationListenerParams {
|
||
pub conversation_id: ConversationId,
|
||
}
|
||
|
||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct RemoveConversationListenerParams {
|
||
pub subscription_id: Uuid,
|
||
}
|
||
|
||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||
#[serde(rename_all = "camelCase")]
|
||
#[serde(tag = "type", content = "data")]
|
||
pub enum InputItem {
|
||
Text {
|
||
text: String,
|
||
},
|
||
/// Pre‑encoded data: URI image.
|
||
Image {
|
||
image_url: String,
|
||
},
|
||
|
||
/// Local image path provided by the user. This will be converted to an
|
||
/// `Image` variant (base64 data URL) during request serialization.
|
||
LocalImage {
|
||
path: PathBuf,
|
||
},
|
||
}
|
||
|
||
// TODO(mbolin): Need test to ensure these constants match the enum variants.
|
||
|
||
pub const APPLY_PATCH_APPROVAL_METHOD: &str = "applyPatchApproval";
|
||
pub const EXEC_COMMAND_APPROVAL_METHOD: &str = "execCommandApproval";
|
||
|
||
/// Request initiated from the server and sent to the client.
|
||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||
#[serde(tag = "method", rename_all = "camelCase")]
|
||
pub enum ServerRequest {
|
||
/// Request to approve a patch.
|
||
ApplyPatchApproval {
|
||
#[serde(rename = "id")]
|
||
request_id: RequestId,
|
||
params: ApplyPatchApprovalParams,
|
||
},
|
||
/// Request to exec a command.
|
||
ExecCommandApproval {
|
||
#[serde(rename = "id")]
|
||
request_id: RequestId,
|
||
params: ExecCommandApprovalParams,
|
||
},
|
||
}
|
||
|
||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||
pub struct ApplyPatchApprovalParams {
|
||
pub conversation_id: ConversationId,
|
||
/// Use to correlate this with [codex_core::protocol::PatchApplyBeginEvent]
|
||
/// and [codex_core::protocol::PatchApplyEndEvent].
|
||
pub call_id: String,
|
||
pub file_changes: HashMap<PathBuf, FileChange>,
|
||
/// Optional explanatory reason (e.g. request for extra write access).
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub reason: Option<String>,
|
||
/// When set, the agent is asking the user to allow writes under this root
|
||
/// for the remainder of the session (unclear if this is honored today).
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub grant_root: Option<PathBuf>,
|
||
}
|
||
|
||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||
pub struct ExecCommandApprovalParams {
|
||
pub conversation_id: ConversationId,
|
||
/// Use to correlate this with [codex_core::protocol::ExecCommandBeginEvent]
|
||
/// and [codex_core::protocol::ExecCommandEndEvent].
|
||
pub call_id: String,
|
||
pub command: Vec<String>,
|
||
pub cwd: PathBuf,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub reason: Option<String>,
|
||
}
|
||
|
||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||
pub struct ExecCommandApprovalResponse {
|
||
pub decision: ReviewDecision,
|
||
}
|
||
|
||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||
pub struct ApplyPatchApprovalResponse {
|
||
pub decision: ReviewDecision,
|
||
}
|
||
|
||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct LoginChatGptCompleteNotification {
|
||
pub login_id: Uuid,
|
||
pub success: bool,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub error: Option<String>,
|
||
}
|
||
|
||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct AuthStatusChangeNotification {
|
||
/// Current authentication method; omitted if signed out.
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub auth_method: Option<AuthMode>,
|
||
}
|
||
|
||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS, Display)]
|
||
#[serde(tag = "type", content = "data", rename_all = "snake_case")]
|
||
#[strum(serialize_all = "snake_case")]
|
||
pub enum ServerNotification {
|
||
/// Authentication status changed
|
||
AuthStatusChange(AuthStatusChangeNotification),
|
||
|
||
/// ChatGPT login flow completed
|
||
LoginChatGptComplete(LoginChatGptCompleteNotification),
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use pretty_assertions::assert_eq;
|
||
use serde_json::json;
|
||
|
||
#[test]
|
||
fn serialize_new_conversation() {
|
||
let request = ClientRequest::NewConversation {
|
||
request_id: RequestId::Integer(42),
|
||
params: NewConversationParams {
|
||
model: Some("gpt-5".to_string()),
|
||
profile: None,
|
||
cwd: None,
|
||
approval_policy: Some(AskForApproval::OnRequest),
|
||
sandbox: None,
|
||
config: None,
|
||
base_instructions: None,
|
||
include_plan_tool: None,
|
||
include_apply_patch_tool: None,
|
||
},
|
||
};
|
||
assert_eq!(
|
||
json!({
|
||
"method": "newConversation",
|
||
"id": 42,
|
||
"params": {
|
||
"model": "gpt-5",
|
||
"approvalPolicy": "on-request"
|
||
}
|
||
}),
|
||
serde_json::to_value(&request).unwrap(),
|
||
);
|
||
}
|
||
}
|