[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:
charlesgong-openai
2026-06-16 19:22:57 -07:00
committed by GitHub
Unverified
parent 6f77491e95
commit 1883dedc0e
10 changed files with 472 additions and 75 deletions
+18 -2
View File
@@ -18,11 +18,18 @@ pub struct PluginManifest<Resource> {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PluginManifestPaths<Resource> {
pub skills: Option<Resource>,
pub mcp_servers: Option<Resource>,
pub mcp_servers: Option<PluginManifestMcpServers<Resource>>,
pub apps: Option<Resource>,
pub hooks: Option<PluginManifestHooks<Resource>>,
}
/// MCP server declarations embedded in or referenced by a plugin manifest.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PluginManifestMcpServers<Resource> {
Path(Resource),
Object(String),
}
/// Hook declarations embedded in or referenced by a plugin manifest.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PluginManifestHooks<Resource> {
@@ -109,6 +116,15 @@ impl<Resource> PluginManifest<Resource> {
Some(PluginManifestHooks::Inline(hooks)) => Some(PluginManifestHooks::Inline(hooks)),
None => None,
};
let mcp_servers = match mcp_servers {
Some(PluginManifestMcpServers::Path(path)) => {
Some(PluginManifestMcpServers::Path(map(path)?))
}
Some(PluginManifestMcpServers::Object(servers)) => {
Some(PluginManifestMcpServers::Object(servers))
}
None => None,
};
let interface = match interface {
Some(interface) => {
let PluginManifestInterface {
@@ -157,7 +173,7 @@ impl<Resource> PluginManifest<Resource> {
keywords,
paths: PluginManifestPaths {
skills: skills.map(&mut map).transpose()?,
mcp_servers: mcp_servers.map(&mut map).transpose()?,
mcp_servers,
apps: apps.map(&mut map).transpose()?,
hooks,
},
+7 -3
View File
@@ -4,6 +4,7 @@ use super::ResolvedPluginError;
use crate::manifest::PluginManifest;
use crate::manifest::PluginManifestHooks;
use crate::manifest::PluginManifestInterface;
use crate::manifest::PluginManifestMcpServers;
use crate::manifest::PluginManifestPaths;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
@@ -37,7 +38,7 @@ fn environment_descriptor_binds_every_manifest_resource() {
keywords: Vec::new(),
paths: PluginManifestPaths {
skills: Some(skills.clone()),
mcp_servers: Some(mcp_servers.clone()),
mcp_servers: Some(PluginManifestMcpServers::Path(mcp_servers.clone())),
apps: Some(apps.clone()),
hooks: Some(PluginManifestHooks::Paths(vec![hooks.clone()])),
},
@@ -71,7 +72,10 @@ fn environment_descriptor_binds_every_manifest_resource() {
keywords: Vec::new(),
paths: PluginManifestPaths {
skills: Some(resource("executor-1", skills)),
mcp_servers: Some(resource("executor-1", mcp_servers)),
mcp_servers: Some(PluginManifestMcpServers::Path(resource(
"executor-1",
mcp_servers,
))),
apps: Some(resource("executor-1", apps)),
hooks: Some(PluginManifestHooks::Paths(vec![resource(
"executor-1",
@@ -100,7 +104,7 @@ fn environment_descriptor_rejects_resources_outside_package_root() {
keywords: Vec::new(),
paths: PluginManifestPaths {
skills: None,
mcp_servers: Some(outside.clone()),
mcp_servers: Some(PluginManifestMcpServers::Path(outside.clone())),
apps: None,
hooks: None,
},