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:
jif
2026-11-02 15:28:37 +00:00
committed by copyberry
parent 18c6dbe52b
commit 517960e0ed
6 changed files with 167 additions and 9 deletions
+1
View File
@@ -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<_>>(),
@@ -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]
+12 -1
View File
@@ -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
}
}
}