diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 84b352c087..49575db9b6 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -690,7 +690,6 @@ pub fn set_default_oss_provider(codex_home: &Path, provider: &str) -> std::io::R #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)] #[schemars(deny_unknown_fields)] pub struct ConfigToml { - pub test: Option, /// Optional override of model selection. pub model: Option, /// Review model override used by the `/review` feature. @@ -748,6 +747,7 @@ pub struct ConfigToml { /// Definition for MCP servers that Codex can reach out to for tool calls. #[serde(default)] + // Uses the raw MCP input shape (custom deserialization) rather than `McpServerConfig`. #[schemars(schema_with = "crate::config::schema::mcp_servers_schema")] pub mcp_servers: HashMap, @@ -816,6 +816,7 @@ pub struct ConfigToml { /// Centralized feature flags (new). Prefer this over individual toggles. #[serde(default)] + // Injects known feature keys into the schema and forbids unknown keys. #[schemars(schema_with = "crate::config::schema::features_schema")] pub features: Option, diff --git a/codex-rs/core/src/config/profile.rs b/codex-rs/core/src/config/profile.rs index 0507c90f20..54f9a08ad0 100644 --- a/codex-rs/core/src/config/profile.rs +++ b/codex-rs/core/src/config/profile.rs @@ -34,6 +34,7 @@ pub struct ConfigProfile { pub analytics: Option, /// Optional feature toggles scoped to this profile. #[serde(default)] + // Injects known feature keys into the schema and forbids unknown keys. #[schemars(schema_with = "crate::config::schema::features_schema")] pub features: Option, pub oss_provider: Option, diff --git a/codex-rs/core/src/config/schema.rs b/codex-rs/core/src/config/schema.rs index 0b15722b3f..86513d0b6d 100644 --- a/codex-rs/core/src/config/schema.rs +++ b/codex-rs/core/src/config/schema.rs @@ -1,7 +1,7 @@ #[cfg(test)] use crate::config::ConfigToml; +use crate::config::types::RawMcpServerConfig; use crate::features::FEATURES; -use schemars::JsonSchema; use schemars::r#gen::SchemaGenerator; #[cfg(test)] use schemars::r#gen::SchemaSettings; @@ -11,13 +11,10 @@ use schemars::schema::ObjectValidation; use schemars::schema::RootSchema; use schemars::schema::Schema; use schemars::schema::SchemaObject; -use schemars::schema::SubschemaValidation; -use serde::Deserialize; -use serde::Serialize; -use std::collections::HashMap; #[cfg(test)] use std::path::Path; +/// Build the config schema used by the fixture test. #[cfg(test)] pub(crate) fn config_schema() -> RootSchema { SchemaSettings::draft07() @@ -28,6 +25,7 @@ pub(crate) fn config_schema() -> RootSchema { .into_root_schema_for::() } +/// Write the config schema fixture to disk. #[cfg(test)] pub(crate) fn write_config_schema(out_path: &Path) -> anyhow::Result<()> { let schema = config_schema(); @@ -36,6 +34,7 @@ pub(crate) fn write_config_schema(out_path: &Path) -> anyhow::Result<()> { Ok(()) } +/// Schema for the `[features]` map with known keys only. pub(crate) fn features_schema(schema_gen: &mut SchemaGenerator) -> Schema { let mut object = SchemaObject { instance_type: Some(InstanceType::Object.into()), @@ -54,6 +53,7 @@ pub(crate) fn features_schema(schema_gen: &mut SchemaGenerator) -> Schema { Schema::Object(object) } +/// Schema for the `[mcp_servers]` map using the raw input shape. pub(crate) fn mcp_servers_schema(schema_gen: &mut SchemaGenerator) -> Schema { let mut object = SchemaObject { instance_type: Some(InstanceType::Object.into()), @@ -61,7 +61,7 @@ pub(crate) fn mcp_servers_schema(schema_gen: &mut SchemaGenerator) -> Schema { }; let validation = ObjectValidation { - additional_properties: Some(Box::new(mcp_server_schema(schema_gen))), + additional_properties: Some(Box::new(schema_gen.subschema_for::())), ..Default::default() }; object.object = Some(Box::new(validation)); @@ -69,70 +69,6 @@ pub(crate) fn mcp_servers_schema(schema_gen: &mut SchemaGenerator) -> Schema { Schema::Object(object) } -fn mcp_server_schema(schema_gen: &mut SchemaGenerator) -> Schema { - let server = SchemaObject { - subschemas: Some(Box::new(SubschemaValidation { - one_of: Some(vec![ - schema_gen.subschema_for::(), - schema_gen.subschema_for::(), - ]), - ..Default::default() - })), - ..Default::default() - }; - Schema::Object(server) -} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -#[schemars(deny_unknown_fields)] -struct McpServerStdioSchema { - command: String, - #[serde(default)] - args: Option>, - #[serde(default)] - env: Option>, - #[serde(default)] - env_vars: Option>, - #[serde(default)] - cwd: Option, - #[serde(default)] - enabled: Option, - #[serde(default)] - startup_timeout_sec: Option, - #[serde(default)] - startup_timeout_ms: Option, - #[serde(default)] - tool_timeout_sec: Option, - #[serde(default)] - enabled_tools: Option>, - #[serde(default)] - disabled_tools: Option>, -} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -#[schemars(deny_unknown_fields)] -struct McpServerStreamableHttpSchema { - url: String, - #[serde(default)] - bearer_token_env_var: Option, - #[serde(default)] - http_headers: Option>, - #[serde(default)] - env_http_headers: Option>, - #[serde(default)] - enabled: Option, - #[serde(default)] - startup_timeout_sec: Option, - #[serde(default)] - startup_timeout_ms: Option, - #[serde(default)] - tool_timeout_sec: Option, - #[serde(default)] - enabled_tools: Option>, - #[serde(default)] - disabled_tools: Option>, -} - #[cfg(test)] mod tests { use super::*; diff --git a/codex-rs/core/src/config/types.rs b/codex-rs/core/src/config/types.rs index 2a705a75d7..d30c1f6e25 100644 --- a/codex-rs/core/src/config/types.rs +++ b/codex-rs/core/src/config/types.rs @@ -49,47 +49,51 @@ pub struct McpServerConfig { pub disabled_tools: Option>, } +// Raw MCP config shape used for deserialization and JSON Schema generation. +// Keep this in sync with the validation logic in `McpServerConfig`. +#[derive(Deserialize, Clone, JsonSchema)] +#[schemars(deny_unknown_fields)] +pub(crate) struct RawMcpServerConfig { + // stdio + pub command: Option, + #[serde(default)] + pub args: Option>, + #[serde(default)] + pub env: Option>, + #[serde(default)] + pub env_vars: Option>, + #[serde(default)] + pub cwd: Option, + pub http_headers: Option>, + #[serde(default)] + pub env_http_headers: Option>, + + // streamable_http + pub url: Option, + pub bearer_token: Option, + pub bearer_token_env_var: Option, + + // shared + #[serde(default)] + pub startup_timeout_sec: Option, + #[serde(default)] + pub startup_timeout_ms: Option, + #[serde(default, with = "option_duration_secs")] + #[schemars(with = "Option")] + pub tool_timeout_sec: Option, + #[serde(default)] + pub enabled: Option, + #[serde(default)] + pub enabled_tools: Option>, + #[serde(default)] + pub disabled_tools: Option>, +} + impl<'de> Deserialize<'de> for McpServerConfig { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { - #[derive(Deserialize, Clone)] - struct RawMcpServerConfig { - // stdio - command: Option, - #[serde(default)] - args: Option>, - #[serde(default)] - env: Option>, - #[serde(default)] - env_vars: Option>, - #[serde(default)] - cwd: Option, - http_headers: Option>, - #[serde(default)] - env_http_headers: Option>, - - // streamable_http - url: Option, - bearer_token: Option, - bearer_token_env_var: Option, - - // shared - #[serde(default)] - startup_timeout_sec: Option, - #[serde(default)] - startup_timeout_ms: Option, - #[serde(default, with = "option_duration_secs")] - tool_timeout_sec: Option, - #[serde(default)] - enabled: Option, - #[serde(default)] - enabled_tools: Option>, - #[serde(default)] - disabled_tools: Option>, - } - let mut raw = RawMcpServerConfig::deserialize(deserializer)?; let startup_timeout_sec = match (raw.startup_timeout_sec, raw.startup_timeout_ms) { diff --git a/docs/config.schema.json b/docs/config.schema.json index ccb21d9041..bb8083939f 100644 --- a/docs/config.schema.json +++ b/docs/config.schema.json @@ -182,14 +182,7 @@ "default": {}, "type": "object", "additionalProperties": { - "oneOf": [ - { - "$ref": "#/definitions/McpServerStdioSchema" - }, - { - "$ref": "#/definitions/McpServerStreamableHttpSchema" - } - ] + "$ref": "#/definitions/RawMcpServerConfig" } }, "model": { @@ -626,142 +619,6 @@ } ] }, - "McpServerStdioSchema": { - "description": "Schema-only representations of MCP server configs.\n\nKeep these in sync with the custom MCP config parsing in `crate::config::types::McpServerConfig`.", - "type": "object", - "required": [ - "command" - ], - "properties": { - "args": { - "default": null, - "type": "array", - "items": { - "type": "string" - } - }, - "command": { - "type": "string" - }, - "cwd": { - "default": null, - "type": "string" - }, - "disabled_tools": { - "default": null, - "type": "array", - "items": { - "type": "string" - } - }, - "enabled": { - "default": null, - "type": "boolean" - }, - "enabled_tools": { - "default": null, - "type": "array", - "items": { - "type": "string" - } - }, - "env": { - "default": null, - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "env_vars": { - "default": null, - "type": "array", - "items": { - "type": "string" - } - }, - "startup_timeout_ms": { - "default": null, - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "startup_timeout_sec": { - "default": null, - "type": "number", - "format": "double" - }, - "tool_timeout_sec": { - "default": null, - "type": "number", - "format": "double" - } - }, - "additionalProperties": false - }, - "McpServerStreamableHttpSchema": { - "type": "object", - "required": [ - "url" - ], - "properties": { - "bearer_token_env_var": { - "default": null, - "type": "string" - }, - "disabled_tools": { - "default": null, - "type": "array", - "items": { - "type": "string" - } - }, - "enabled": { - "default": null, - "type": "boolean" - }, - "enabled_tools": { - "default": null, - "type": "array", - "items": { - "type": "string" - } - }, - "env_http_headers": { - "default": null, - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "http_headers": { - "default": null, - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "startup_timeout_ms": { - "default": null, - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "startup_timeout_sec": { - "default": null, - "type": "number", - "format": "double" - }, - "tool_timeout_sec": { - "default": null, - "type": "number", - "format": "double" - }, - "url": { - "type": "string" - } - }, - "additionalProperties": false - }, "ModelProviderInfo": { "description": "Serializable representation of a provider definition.", "type": "object", @@ -1080,6 +937,96 @@ }, "additionalProperties": false }, + "RawMcpServerConfig": { + "type": "object", + "properties": { + "args": { + "default": null, + "type": "array", + "items": { + "type": "string" + } + }, + "bearer_token": { + "type": "string" + }, + "bearer_token_env_var": { + "type": "string" + }, + "command": { + "type": "string" + }, + "cwd": { + "default": null, + "type": "string" + }, + "disabled_tools": { + "default": null, + "type": "array", + "items": { + "type": "string" + } + }, + "enabled": { + "default": null, + "type": "boolean" + }, + "enabled_tools": { + "default": null, + "type": "array", + "items": { + "type": "string" + } + }, + "env": { + "default": null, + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "env_http_headers": { + "default": null, + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "env_vars": { + "default": null, + "type": "array", + "items": { + "type": "string" + } + }, + "http_headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "startup_timeout_ms": { + "default": null, + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "startup_timeout_sec": { + "default": null, + "type": "number", + "format": "double" + }, + "tool_timeout_sec": { + "default": null, + "type": "number", + "format": "double" + }, + "url": { + "type": "string" + } + }, + "additionalProperties": false + }, "ReasoningEffort": { "description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "type": "string",