mirror of
https://github.com/openai/codex.git
synced 2026-11-07 20:30:33 +00:00
Package the message board MCP connection as a plugin (#58908)
## What changed Add a `message-board` plugin with connection metadata for the HTTP MCP server. It defaults to `http://127.0.0.1:7777/mcp`, uses `CODEX_MESSAGE_BOARD_TOKEN` for bearer authentication, and marks the server as required. Document installation and endpoint overrides through `mcp_servers.codex_message_board`. Installing the plugin does not start the server. ## Testing Extend the HTTP board integration scenario to cover both direct configuration and plugin installation, verifying that a child can post a decision and its restarted parent can discover the same board and read it. Add a plugin context snapshot and include the plugin resources in Bazel test data. GitOrigin-RevId: d6cf797caaa4dbdcd45114c2ad0a6bd695cf056f
This commit is contained in:
@@ -30,6 +30,7 @@ codex_rust_crate(
|
||||
"CARGO_MANIFEST_DIR": "codex-rs/core",
|
||||
},
|
||||
test_data_extra = [
|
||||
"//codex-rs/message-board-mcp:package-files",
|
||||
"config.schema.json",
|
||||
] + glob(["src/**/snapshots/**"]) + [
|
||||
],
|
||||
|
||||
@@ -5,8 +5,10 @@ use pretty_assertions::assert_eq;
|
||||
use std::sync::Arc;
|
||||
use tokio::io::AsyncBufReadExt;
|
||||
|
||||
#[test_case::test_case(false; "config")]
|
||||
#[test_case::test_case(true; "plugin")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn http_board_is_shared_with_children() -> anyhow::Result<()> {
|
||||
async fn http_board_is_shared_with_children(plugin: bool) -> anyhow::Result<()> {
|
||||
let server = responses::start_mock_server().await;
|
||||
let home = Arc::new(tempfile::tempdir()?);
|
||||
let sqlite = codex_state::SqliteConfig::new_for_testing(home.path().to_path_buf().try_into()?);
|
||||
@@ -28,6 +30,26 @@ async fn http_board_is_shared_with_children() -> anyhow::Result<()> {
|
||||
.context("server readiness")?;
|
||||
let ready: Value = serde_json::from_str(&ready)?;
|
||||
let endpoint = json!({"url":ready["url"],"http_headers":{"Authorization":"Bearer http-board-test-token","Host":"board.example"},"required":true,"default_tools_approval_mode":"approve"});
|
||||
if plugin {
|
||||
let manifest = codex_utils_cargo_bin::find_resource!(
|
||||
"../message-board-mcp/plugin/.codex-plugin/plugin.json"
|
||||
)?;
|
||||
let declaration_path =
|
||||
codex_utils_cargo_bin::find_resource!("../message-board-mcp/plugin/.mcp.json")?;
|
||||
let installed = home.path().join("plugins/cache/test/message-board/local");
|
||||
std::fs::create_dir_all(installed.join(".codex-plugin"))?;
|
||||
std::fs::copy(manifest, installed.join(".codex-plugin/plugin.json"))?;
|
||||
let mut declaration: Value = serde_json::from_slice(&std::fs::read(declaration_path)?)?;
|
||||
declaration["mcpServers"]["codex_message_board"] = endpoint.clone();
|
||||
std::fs::write(
|
||||
installed.join(".mcp.json"),
|
||||
serde_json::to_vec(&declaration)?,
|
||||
)?;
|
||||
std::fs::write(
|
||||
home.path().join("config.toml"),
|
||||
"[features]\nplugins = true\n[plugins.\"message-board@test\"]\nenabled = true\n",
|
||||
)?;
|
||||
}
|
||||
let configured_endpoint = endpoint.clone();
|
||||
let configured_sqlite = sqlite.clone();
|
||||
let configure_board = move |config: &mut codex_core::config::Config| {
|
||||
@@ -37,12 +59,14 @@ async fn http_board_is_shared_with_children() -> anyhow::Result<()> {
|
||||
.features
|
||||
.disable(Feature::AgentMessageBoard)
|
||||
.expect("disable native board");
|
||||
let mut servers = config.mcp_servers.get().clone();
|
||||
servers.insert(
|
||||
"codex_message_board".into(),
|
||||
serde_json::from_value(configured_endpoint.clone()).expect("HTTP config"),
|
||||
);
|
||||
config.mcp_servers.set(servers).expect("configure endpoint");
|
||||
if !plugin {
|
||||
let mut servers = config.mcp_servers.get().clone();
|
||||
servers.insert(
|
||||
"codex_message_board".into(),
|
||||
serde_json::from_value(configured_endpoint.clone()).expect("HTTP config"),
|
||||
);
|
||||
config.mcp_servers.set(servers).expect("configure endpoint");
|
||||
}
|
||||
};
|
||||
let mut builder = test_codex()
|
||||
.with_home(home)
|
||||
@@ -194,7 +218,11 @@ async fn http_board_is_shared_with_children() -> anyhow::Result<()> {
|
||||
}
|
||||
}
|
||||
insta::assert_snapshot!(
|
||||
"agent_message_board_mcp_context",
|
||||
if plugin {
|
||||
"agent_message_board_plugin_context"
|
||||
} else {
|
||||
"agent_message_board_mcp_context"
|
||||
},
|
||||
context_snapshot::format_context_snapshot(
|
||||
"A child posts through the HTTP MCP executable; its restarted parent discovers the same board and reads the decision.",
|
||||
&bodies.iter().map(SnapshotEntry::body).collect::<Vec<_>>(),
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
---
|
||||
source: core/tests/suite/scenarios_agent_message_board_http.rs
|
||||
expression: "context_snapshot::format_context_snapshot(\"A child posts through the HTTP MCP executable; its restarted parent discovers the same board and reads the decision.\",\n&bodies.iter().map(SnapshotEntry::body).collect::<Vec<_>>(),\n&ContextSnapshotOptions::default().rewrite_known_segments(),)"
|
||||
---
|
||||
Scenario: A child posts through the HTTP MCP executable; its restarted parent discovers the same board and reads the decision.
|
||||
|
||||
## Window 1
|
||||
-- request 1 (request) --
|
||||
00:message/developer:
|
||||
<MODEL_INSTRUCTIONS>
|
||||
01:message/developer[3]:
|
||||
[01] <PERMISSIONS_INSTRUCTIONS>
|
||||
[02] <PLUGINS_INSTRUCTIONS>
|
||||
[03] <tools>
|
||||
Deferred tool namespaces:
|
||||
- mcp__codex_message_board
|
||||
</tools>
|
||||
02:message/developer:
|
||||
You are an agent in a team of agents collaborating to complete a task.
|
||||
|
||||
You can spawn sub-agents to handle subtasks, and those sub-agents can spawn their own sub-agents. All ag...nd capable, and have access to the same set of tools. [hash=876a93c54d3f61d6]
|
||||
|
||||
You can use `spawn_agent` to create a new agent, `followup_task` to give an existing agent a new task an... `send_message` to pass a message to a running agent. [hash=824c12425c7626dc]
|
||||
Child agents can also spawn their own sub-agents.
|
||||
|
||||
When you provide a response in the final channel, that content is immediately delivered back to your parent agent.
|
||||
<OMITTED 15 LINES; ~225 TOKENS; hash=fa366829bdb214be>
|
||||
- All agents use the same current working directory.
|
||||
- As a result, edits made by one agent are immediately visible to all other agents.
|
||||
|
||||
When calling `wait_agent`, prefer longer waits (minutes) to avoid busy polling.
|
||||
|
||||
There are 4 available concurrency slots, meaning that up to 4 agents can be active at once, including you.
|
||||
|
||||
Full-history forks (`fork_turns` omitted or `"all"`) inherit the parent model and reasoning effort and d...uctions; when doing so, set `fork_turns` to `"none"`. [hash=18ccf7e93ecabab9]
|
||||
03:message/developer:
|
||||
<MULTI_AGENT_MODE>
|
||||
04:message/user:
|
||||
<ENVIRONMENT_CONTEXT>
|
||||
05:agent_message
|
||||
06:message/assistant:
|
||||
Done.
|
||||
07:message/user:
|
||||
Find the board tools and post your decision.
|
||||
-- request 2 (request) --
|
||||
08:tool_search_call
|
||||
09:tool_search_output
|
||||
-- request 3 (request) --
|
||||
10:function_call/mcp__codex_message_board.post:{"new_channel_name":"design","text":"Worker's durable decision."}
|
||||
11:function_call_output:Wall time: <DURATION> seconds
|
||||
Output:
|
||||
{"author":"/root/worker","channel_name":"design","created_at":"<TIMESTAMP>","message_id":"<UUID 1>","thread_id":"<UUID 1>"}
|
||||
|
||||
## Window 2 (after request 3: input diverged at item 02, settings changed (tools))
|
||||
-- request 4 (request) --
|
||||
00:message/developer:
|
||||
<MODEL_INSTRUCTIONS>
|
||||
01:message/developer[3]:
|
||||
[01] <PERMISSIONS_INSTRUCTIONS>
|
||||
[02] <PLUGINS_INSTRUCTIONS>
|
||||
[03] <tools>
|
||||
Deferred tool namespaces:
|
||||
- mcp__codex_message_board
|
||||
</tools>
|
||||
02:message/developer:
|
||||
You are `/root`, the primary agent in a team of agents collaborating to fulfill the user's goals.
|
||||
|
||||
At the start of your turn, you are the active agent.
|
||||
You can spawn sub-agents to handle subtasks, and those sub-agents can spawn their own sub-agents.
|
||||
All agents in the team, including the agents that you can assign tasks to, are equally intelligent and capable, and have access to the same set of tools.
|
||||
|
||||
You can use `spawn_agent` to create a new agent, `followup_task` to give an existing agent a new task an...message to a running agent without triggering a turn. [hash=7d40ff8b58977202]
|
||||
Child agents can also spawn their own sub-agents.
|
||||
<OMITTED 16 LINES; ~234 TOKENS; hash=b2e94283a8c22327>
|
||||
- All agents use the same current working directory.
|
||||
- As a result, edits made by one agent are immediately visible to all other agents.
|
||||
|
||||
When calling `wait_agent`, prefer longer waits (minutes) to avoid busy polling.
|
||||
|
||||
There are 4 available concurrency slots, meaning that up to 4 agents can be active at once, including you.
|
||||
|
||||
Full-history forks (`fork_turns` omitted or `"all"`) inherit the parent model and reasoning effort and d...uctions; when doing so, set `fork_turns` to `"none"`. [hash=18ccf7e93ecabab9]
|
||||
03:message/developer:
|
||||
<MULTI_AGENT_MODE>
|
||||
04:message/user:
|
||||
<ENVIRONMENT_CONTEXT>
|
||||
05:message/user:
|
||||
Spawn a worker and finish your turn.
|
||||
06:function_call/collaboration.spawn_agent:{"fork_turns":"none","message":"Say ready.","task_name":"worker"}
|
||||
07:function_call_output:{"task_name":"/root/worker"}
|
||||
08:message/assistant:
|
||||
Done.
|
||||
09:message/user:
|
||||
<ENVIRONMENT_CONTEXT>
|
||||
10:message/user:
|
||||
Find the board tools and read the worker's decision.
|
||||
-- request 5 (request) --
|
||||
11:tool_search_call
|
||||
12:tool_search_output
|
||||
-- request 6 (request) --
|
||||
13:function_call/mcp__codex_message_board.search_posts:{"channel_name":"design"}
|
||||
14:function_call_output:Wall time: <DURATION> seconds
|
||||
Output:
|
||||
{"has_more":false,"n_returned":1,"next_cursor":null,"results":[{"author":"/root/worker","channel_name":"...ecision.","thread_id":"<UUID 1>","truncated":false}]} [hash=0322071c1fb9baa5]
|
||||
@@ -73,4 +73,15 @@ for a remote hostname (repeat for multiple names); loopback hosts remain allowed
|
||||
Library callers configure this through `StreamableHttpServerConfig`. Normal MCP
|
||||
approval policy applies.
|
||||
Deduplication is scoped to MCP sessions, without guarantees across reconnects or for
|
||||
stateless clients. Plugin packaging, defaults and in-memory launching are separate.
|
||||
stateless clients.
|
||||
|
||||
## Plugin
|
||||
|
||||
Install `plugin/` through the normal plugin flow. Its MCP declaration defaults to
|
||||
`http://127.0.0.1:7777/mcp` and `CODEX_MESSAGE_BOARD_TOKEN`. The package contains
|
||||
connection metadata only and can be distributed independently of the server.
|
||||
Override `mcp_servers.codex_message_board` in ordinary config to point the same
|
||||
plugin at a local or deployed implementation of the contract.
|
||||
|
||||
Installing the plugin does not start the HTTP process. Generic app-server default
|
||||
installation and local in-memory launching remain separate work.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "message-board",
|
||||
"version": "0.1.0",
|
||||
"description": "Shared agent discussion boards through MCP."
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"codex_message_board": {
|
||||
"url": "http://127.0.0.1:7777/mcp",
|
||||
"bearer_token_env_var": "CODEX_MESSAGE_BOARD_TOKEN",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user