mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
[codex] Support object-valued plugin MCP manifests (#28580)
## Summary
This fixes plugin manifest parsing for MCP servers declared as an object
directly in `plugin.json`.
Before this change, Codex modeled `mcpServers` as only a string path,
for example:
```json
{
"name": "counter-sample",
"version": "1.1.1",
"mcpServers": "./.mcp.json"
}
```
Some migrated plugins instead provide the server map directly in the
manifest:
```json
{
"name": "counter-sample",
"version": "1.1.1",
"description": "Plugin that declares MCP servers in the manifest",
"mcpServers": {
"counter": {
"type": "http",
"url": "https://sample.example/counter/mcp"
}
}
}
```
That object form previously failed during install/load with an error
like:
```text
failed to parse plugin manifest: invalid type: map, expected a string
```
## What changed
- Add a manifest representation for `mcpServers` as either
`Path(Resource)` or `Object(map)`.
- Parse `plugin.json` `mcpServers` as either a string path or an object.
- Route object-valued MCP server maps through the existing plugin MCP
config parser instead of adding a second parser.
- Apply existing per-plugin MCP server policy to object-valued MCP
servers the same way as file-backed MCP servers.
- Include object-valued MCP server names in plugin telemetry/capability
metadata.
- Support object-valued MCP config for executor plugins without
requiring a `.mcp.json` filesystem read.
- Update the bundled plugin-creator validator and `plugin-json-spec.md`
so generated-plugin validation accepts the same object-valued shape.
## Compatibility
Existing plugin manifests that use `"mcpServers": "./.mcp.json"`
continue to work. Plugins can now also use the object shape shown above.
## Tests
Added coverage for the new manifest attribute shape at the install,
normal load, telemetry, and executor-provider layers:
- `install_accepts_manifest_mcp_server_objects`
- `load_plugins_loads_manifest_mcp_server_objects`
- `plugin_telemetry_metadata_uses_manifest_mcp_server_objects`
- `reads_manifest_object_config_without_executor_file_system_access`
Also smoke-tested the plugin-creator validator against both supported
forms:
- `mcpServers` as a direct object in `plugin.json`
- `mcpServers` as `"./.mcp.json"` with a companion `.mcp.json`
## Validation
- `just test -p codex-plugin`
- `just test -p codex-core-plugins`
- `just test -p codex-mcp-extension`
- `just bazel-lock-update`
- `just bazel-lock-check`
- `just fmt`
- `git diff --check`
- Focused rename/object-form rerun: `just test -p codex-core-plugins
manager::tests::load_plugins_loads_manifest_mcp_server_objects
manager::tests::plugin_telemetry_metadata_uses_manifest_mcp_server_objects
store::tests::install_accepts_manifest_mcp_server_objects`
- Focused executor rerun: `just test -p codex-mcp-extension
executor_plugin::provider::tests::reads_manifest_object_config_without_executor_file_system_access`
- `python3
codex-rs/skills/src/assets/samples/plugin-creator/scripts/validate_plugin.py
/private/tmp/codex-validator-object`
- `python3
codex-rs/skills/src/assets/samples/plugin-creator/scripts/validate_plugin.py
/private/tmp/codex-validator-path`
This commit is contained in:
1 parent
6f77491e95
commit
1883dedc0e
10 files changed
+472
-75
No files matched your search
@@ -682,6 +682,70 @@ async fn load_plugins_loads_default_skills_and_mcp_servers() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_plugins_loads_manifest_mcp_server_objects() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
let plugin_root = codex_home
|
||||
.path()
|
||||
.join("plugins/cache")
|
||||
.join("test/counter-sample/local");
|
||||
|
||||
write_file(
|
||||
&plugin_root.join(".codex-plugin/plugin.json"),
|
||||
r#"{
|
||||
"name": "counter-sample",
|
||||
"version": "1.1.1",
|
||||
"description": "Plugin that declares MCP servers in the manifest",
|
||||
"mcpServers": {
|
||||
"counter": {
|
||||
"type": "http",
|
||||
"url": "https://sample.example/counter/mcp"
|
||||
}
|
||||
}
|
||||
}"#,
|
||||
);
|
||||
|
||||
let config_toml = r#"
|
||||
[features]
|
||||
plugins = true
|
||||
|
||||
[plugins."counter-sample@test"]
|
||||
enabled = true
|
||||
"#;
|
||||
let outcome =
|
||||
load_plugins_from_config(config_toml, codex_home.path(), /*auth_mode*/ None).await;
|
||||
|
||||
assert_eq!(outcome.plugins()[0].error, None);
|
||||
assert_eq!(
|
||||
outcome.plugins()[0].mcp_servers,
|
||||
HashMap::from([(
|
||||
"counter".to_string(),
|
||||
McpServerConfig {
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "https://sample.example/counter/mcp".to_string(),
|
||||
bearer_token_env_var: None,
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
},
|
||||
environment_id: "local".to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth: None,
|
||||
oauth_resource: None,
|
||||
tools: HashMap::new(),
|
||||
},
|
||||
)])
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_plugins_applies_plugin_mcp_server_policy() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
@@ -1098,6 +1162,47 @@ async fn plugin_telemetry_metadata_uses_default_mcp_config_path() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_telemetry_metadata_uses_manifest_mcp_server_objects() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
let plugin_root = codex_home
|
||||
.path()
|
||||
.join("plugins/cache")
|
||||
.join("test/counter-sample/local");
|
||||
|
||||
write_file(
|
||||
&plugin_root.join(".codex-plugin/plugin.json"),
|
||||
r#"{
|
||||
"name": "counter-sample",
|
||||
"version": "1.1.1",
|
||||
"mcpServers": {
|
||||
"counter": {
|
||||
"type": "http",
|
||||
"url": "https://sample.example/counter/mcp"
|
||||
}
|
||||
}
|
||||
}"#,
|
||||
);
|
||||
|
||||
let metadata = plugin_telemetry_metadata_from_root(
|
||||
&PluginId::parse("counter-sample@test").expect("plugin id should parse"),
|
||||
&plugin_root.abs(),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
metadata.capability_summary,
|
||||
Some(PluginCapabilitySummary {
|
||||
config_name: "counter-sample@test".to_string(),
|
||||
display_name: "counter-sample".to_string(),
|
||||
description: None,
|
||||
has_skills: false,
|
||||
mcp_server_names: vec!["counter".to_string()],
|
||||
app_connector_ids: Vec::new(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn capability_summary_sanitizes_plugin_descriptions_to_one_line() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
|
||||
Reference in new issue
Block a user