Skip to content

Commit ea3e4e1

Browse files
committed
feat: initial work by Codex to create Codex MCP tool call
1 parent 5d924d4 commit ea3e4e1

File tree

6 files changed

+548
-41
lines changed

6 files changed

+548
-41
lines changed

codex-rs/Cargo.lock

Lines changed: 43 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/mcp-server/Cargo.toml

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,9 @@ version = "0.1.0"
44
edition = "2021"
55

66
[dependencies]
7-
#
8-
# codex-core contains optional functionality that is gated behind the "cli"
9-
# feature. Unfortunately there is an unconditional reference to a module that
10-
# is only compiled when the feature is enabled, which breaks the build when
11-
# the default (no-feature) variant is used.
12-
#
13-
# We therefore explicitly enable the "cli" feature when codex-mcp-server pulls
14-
# in codex-core so that the required symbols are present. This does _not_
15-
# change the public API of codex-core – it merely opts into compiling the
16-
# extra, feature-gated source files so the build succeeds.
17-
#
187
codex-core = { path = "../core", features = ["cli"] }
198
mcp-types = { path = "../mcp-types" }
9+
schemars = "0.8.22"
2010
serde = { version = "1", features = ["derive"] }
2111
serde_json = "1"
2212
tracing = { version = "0.1.41", features = ["log"] }
@@ -28,3 +18,6 @@ tokio = { version = "1", features = [
2818
"rt-multi-thread",
2919
"signal",
3020
] }
21+
22+
[dev-dependencies]
23+
pretty_assertions = "1.4.1"
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
//! Configuration object accepted by the `codex` MCP tool-call.
2+
3+
use std::path::PathBuf;
4+
5+
use mcp_types::Tool;
6+
use mcp_types::ToolInputSchema;
7+
use schemars::r#gen::SchemaSettings;
8+
use schemars::JsonSchema;
9+
use serde::Deserialize;
10+
11+
use codex_core::protocol::AskForApproval;
12+
use codex_core::protocol::SandboxPolicy;
13+
14+
/// Client-supplied configuration for a `codex` tool-call.
15+
#[derive(Debug, Clone, Deserialize, JsonSchema)]
16+
#[serde(rename_all = "kebab-case")]
17+
pub(crate) struct CodexToolCallParam {
18+
/// The *initial user prompt* to start the Codex conversation.
19+
pub prompt: String,
20+
21+
/// Optional override for the model name (e.g. "o3", "o4-mini")
22+
#[serde(default, skip_serializing_if = "Option::is_none")]
23+
pub model: Option<String>,
24+
25+
/// Working directory for the session. If relative, it is resolved against
26+
/// the server process's current working directory.
27+
#[serde(default, skip_serializing_if = "Option::is_none")]
28+
pub cwd: Option<String>,
29+
30+
/// Execution approval policy expressed as the kebab-case variant name
31+
/// (`unless-allow-listed`, `auto-edit`, `on-failure`, `never`).
32+
#[serde(default, skip_serializing_if = "Option::is_none")]
33+
pub approval_policy: Option<CodexToolCallApprovalPolicy>,
34+
35+
/// Sandbox permissions using the same string values accepted by the CLI
36+
/// (e.g. "disk-write-cwd", "network-full-access").
37+
#[serde(default, skip_serializing_if = "Option::is_none")]
38+
pub sandbox_permissions: Option<Vec<CodexToolCallSandboxPermission>>,
39+
40+
/// Disable server-side response storage.
41+
#[serde(default, skip_serializing_if = "Option::is_none")]
42+
pub disable_response_storage: Option<bool>,
43+
// Custom system instructions.
44+
// #[serde(default, skip_serializing_if = "Option::is_none")]
45+
// pub instructions: Option<String>,
46+
}
47+
48+
// Create custom enums for use with `CodexToolCallApprovalPolicy` where we
49+
// intentionally exclude docstrings from the generated schema because they
50+
// introduce anyOf in the the generated JSON schema, which makes it more complex
51+
// without adding any real value since we aspire to use self-descriptive names.
52+
53+
#[derive(Debug, Clone, Deserialize, JsonSchema)]
54+
#[serde(rename_all = "kebab-case")]
55+
pub(crate) enum CodexToolCallApprovalPolicy {
56+
AutoEdit,
57+
UnlessAllowListed,
58+
OnFailure,
59+
Never,
60+
}
61+
62+
impl From<CodexToolCallApprovalPolicy> for AskForApproval {
63+
fn from(value: CodexToolCallApprovalPolicy) -> Self {
64+
match value {
65+
CodexToolCallApprovalPolicy::AutoEdit => AskForApproval::AutoEdit,
66+
CodexToolCallApprovalPolicy::UnlessAllowListed => AskForApproval::UnlessAllowListed,
67+
CodexToolCallApprovalPolicy::OnFailure => AskForApproval::OnFailure,
68+
CodexToolCallApprovalPolicy::Never => AskForApproval::Never,
69+
}
70+
}
71+
}
72+
73+
// TODO: Support additional writable folders via a separate property on
74+
// CodexToolCallParam.
75+
76+
#[derive(Debug, Clone, Deserialize, JsonSchema)]
77+
#[serde(rename_all = "kebab-case")]
78+
pub(crate) enum CodexToolCallSandboxPermission {
79+
DiskFullReadAccess,
80+
DiskWriteCwd,
81+
DiskWritePlatformUserTempFolder,
82+
DiskWritePlatformGlobalTempFolder,
83+
DiskFullWriteAccess,
84+
NetworkFullAccess,
85+
}
86+
87+
impl From<CodexToolCallSandboxPermission> for codex_core::protocol::SandboxPermission {
88+
fn from(value: CodexToolCallSandboxPermission) -> Self {
89+
match value {
90+
CodexToolCallSandboxPermission::DiskFullReadAccess => {
91+
codex_core::protocol::SandboxPermission::DiskFullReadAccess
92+
}
93+
CodexToolCallSandboxPermission::DiskWriteCwd => {
94+
codex_core::protocol::SandboxPermission::DiskWriteCwd
95+
}
96+
CodexToolCallSandboxPermission::DiskWritePlatformUserTempFolder => {
97+
codex_core::protocol::SandboxPermission::DiskWritePlatformUserTempFolder
98+
}
99+
CodexToolCallSandboxPermission::DiskWritePlatformGlobalTempFolder => {
100+
codex_core::protocol::SandboxPermission::DiskWritePlatformGlobalTempFolder
101+
}
102+
CodexToolCallSandboxPermission::DiskFullWriteAccess => {
103+
codex_core::protocol::SandboxPermission::DiskFullWriteAccess
104+
}
105+
CodexToolCallSandboxPermission::NetworkFullAccess => {
106+
codex_core::protocol::SandboxPermission::NetworkFullAccess
107+
}
108+
}
109+
}
110+
}
111+
112+
pub(crate) fn create_tool_for_codex_tool_call_param() -> Tool {
113+
let schema = SchemaSettings::draft2019_09()
114+
.with(|s| {
115+
s.inline_subschemas = true;
116+
s.option_add_null_type = false
117+
})
118+
.into_generator()
119+
.into_root_schema_for::<CodexToolCallParam>();
120+
let schema_value =
121+
serde_json::to_value(&schema).expect("Codex tool schema should serialise to JSON");
122+
123+
let tool_input_schema =
124+
serde_json::from_value::<ToolInputSchema>(schema_value).unwrap_or_else(|e| {
125+
panic!("failed to create Tool from schema: {e}");
126+
});
127+
Tool {
128+
name: "codex".to_string(),
129+
input_schema: tool_input_schema,
130+
description: Some(
131+
"Run a Codex session. Accepts configuration parameters matching the Codex Config struct."
132+
.to_string(),
133+
),
134+
annotations: None,
135+
}
136+
}
137+
138+
impl CodexToolCallParam {
139+
/// Returns the initial user prompt to start the Codex conversation and the
140+
/// Config.
141+
pub fn into_config(self) -> std::io::Result<(String, codex_core::config::Config)> {
142+
let Self {
143+
prompt,
144+
model,
145+
cwd,
146+
approval_policy,
147+
sandbox_permissions,
148+
disable_response_storage,
149+
} = self;
150+
let sandbox_policy = sandbox_permissions.map(|perms| {
151+
SandboxPolicy::from(perms.into_iter().map(Into::into).collect::<Vec<_>>())
152+
});
153+
154+
// Build ConfigOverrides recognised by codex-core.
155+
let overrides = codex_core::config::ConfigOverrides {
156+
model,
157+
cwd: cwd.map(PathBuf::from),
158+
approval_policy: approval_policy.map(Into::into),
159+
sandbox_policy,
160+
disable_response_storage,
161+
};
162+
163+
let cfg = codex_core::config::Config::load_with_overrides(overrides)?;
164+
165+
Ok((prompt, cfg))
166+
}
167+
}
168+
169+
#[cfg(test)]
170+
mod tests {
171+
use super::*;
172+
use pretty_assertions::assert_eq;
173+
174+
/// We include a test to verify the exact JSON schema as "executable
175+
/// documentation" for the schema. When can track changes to this test as a
176+
/// way to audit changes to the generated schema.
177+
///
178+
/// Seeing the fully expanded schema makes it easier to casually verify that
179+
/// the generated JSON for enum types such as "approval-policy" is compact.
180+
/// Ideally, modelcontextprotocol/inspector would provide a simpler UI for
181+
/// enum fields versus open string fields to take advantage of this.
182+
///
183+
/// As of 2025-05-04, there is an open PR for this:
184+
/// https://github.com/modelcontextprotocol/inspector/pull/196
185+
#[test]
186+
fn verify_codex_tool_json_schema() {
187+
let tool = create_tool_for_codex_tool_call_param();
188+
let tool_json = serde_json::to_value(&tool).expect("tool serializes");
189+
let expected_tool_json = serde_json::json!({
190+
"name": "codex",
191+
"description": "Run a Codex session. Accepts configuration parameters matching the Codex Config struct.",
192+
"inputSchema": {
193+
"type": "object",
194+
"properties": {
195+
"approval-policy": {
196+
"description": "Execution approval policy expressed as the kebab-case variant name (`unless-allow-listed`, `auto-edit`, `on-failure`, `never`).",
197+
"enum": [
198+
"auto-edit",
199+
"unless-allow-listed",
200+
"on-failure",
201+
"never"
202+
],
203+
"type": "string"
204+
},
205+
"cwd": {
206+
"description": "Working directory for the session. If relative, it is resolved against the server process's current working directory.",
207+
"type": "string"
208+
},
209+
"disable-response-storage": {
210+
"description": "Disable server-side response storage.",
211+
"type": "boolean"
212+
},
213+
"model": {
214+
"description": "Optional override for the model name (e.g. \"o3\", \"o4-mini\")",
215+
"type": "string"
216+
},
217+
"prompt": {
218+
"description": "The *initial user prompt* to start the Codex conversation.",
219+
"type": "string"
220+
},
221+
"sandbox-permissions": {
222+
"description": "Sandbox permissions using the same string values accepted by the CLI (e.g. \"disk-write-cwd\", \"network-full-access\").",
223+
"items": {
224+
"enum": [
225+
"disk-full-read-access",
226+
"disk-write-cwd",
227+
"disk-write-platform-user-temp-folder",
228+
"disk-write-platform-global-temp-folder",
229+
"disk-full-write-access",
230+
"network-full-access"
231+
],
232+
"type": "string"
233+
},
234+
"type": "array"
235+
}
236+
},
237+
"required": [
238+
"prompt"
239+
]
240+
}
241+
});
242+
assert_eq!(expected_tool_json, tool_json);
243+
}
244+
}

0 commit comments

Comments
 (0)