diff --git a/codex-rs/core/BUILD.bazel b/codex-rs/core/BUILD.bazel index 9b2f933b97..6283ce0cf4 100644 --- a/codex-rs/core/BUILD.bazel +++ b/codex-rs/core/BUILD.bazel @@ -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/**"]) + [ ], diff --git a/codex-rs/core/tests/suite/scenarios_agent_message_board_http.rs b/codex-rs/core/tests/suite/scenarios_agent_message_board_http.rs index 54c8c7fe8f..804fa8fd2c 100644 --- a/codex-rs/core/tests/suite/scenarios_agent_message_board_http.rs +++ b/codex-rs/core/tests/suite/scenarios_agent_message_board_http.rs @@ -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::>(), diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__agent_message_board__http__agent_message_board_plugin_context.snap b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__agent_message_board__http__agent_message_board_plugin_context.snap new file mode 100644 index 0000000000..0f9dfb708f --- /dev/null +++ b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__agent_message_board__http__agent_message_board_plugin_context.snap @@ -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::>(),\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: + +01:message/developer[3]: + [01] + [02] + [03] + Deferred tool namespaces: + - mcp__codex_message_board + +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. + + - 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: + +04:message/user: + +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: seconds + Output: + {"author":"/root/worker","channel_name":"design","created_at":"","message_id":"","thread_id":""} + +## Window 2 (after request 3: input diverged at item 02, settings changed (tools)) +-- request 4 (request) -- +00:message/developer: + +01:message/developer[3]: + [01] + [02] + [03] + Deferred tool namespaces: + - mcp__codex_message_board + +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. + + - 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: + +04:message/user: + +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: + +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: seconds + Output: + {"has_more":false,"n_returned":1,"next_cursor":null,"results":[{"author":"/root/worker","channel_name":"...ecision.","thread_id":"","truncated":false}]} [hash=0322071c1fb9baa5] diff --git a/codex-rs/message-board-mcp/README.md b/codex-rs/message-board-mcp/README.md index 86d663f45c..1daa73cc95 100644 --- a/codex-rs/message-board-mcp/README.md +++ b/codex-rs/message-board-mcp/README.md @@ -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. diff --git a/codex-rs/message-board-mcp/plugin/.codex-plugin/plugin.json b/codex-rs/message-board-mcp/plugin/.codex-plugin/plugin.json new file mode 100644 index 0000000000..f7180259a9 --- /dev/null +++ b/codex-rs/message-board-mcp/plugin/.codex-plugin/plugin.json @@ -0,0 +1,5 @@ +{ + "name": "message-board", + "version": "0.1.0", + "description": "Shared agent discussion boards through MCP." +} diff --git a/codex-rs/message-board-mcp/plugin/.mcp.json b/codex-rs/message-board-mcp/plugin/.mcp.json new file mode 100644 index 0000000000..ab42f14758 --- /dev/null +++ b/codex-rs/message-board-mcp/plugin/.mcp.json @@ -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 + } + } +}