Files
codex/codex-rs/message-board-mcp
jif 0712633d37 Release message-board state before HTTP shutdown returns (#58933)
## Why

Connected legacy MCP sessions and HTTP keep-alive connections can retain message-board state after shutdown. Pending board bindings also need cancellation before they can dispatch a tool call.

## What changed

Explicitly close MCP sessions after HTTP drains and track binding factories and handlers until they release their state. Cancel pending bindings during shutdown while allowing already-dispatched board operations to finish. Clarify that `LocalServer::shutdown` waits for cleanup, while dropping the server initiates it.

## Testing

Add regression coverage for shutdown with a connected legacy client, a paused board binding, and an unauthenticated keep-alive connection. Assert that shutdown releases the board host before returning.

GitOrigin-RevId: 7fbc9415e579a21f0c0fcf7bbb7db239b3aea886
2026-11-02 17:16:07 +00:00
..

Message-board MCP adapter

MessageBoardMcpServer implements rmcp::ServerHandler over the existing codex-message-board tool factory. It reuses the selected tools' descriptions, input schemas, validation, permission checks, output budgets and storage behavior. It has no dependency on codex-core.

A host supplies an authorized board connection, caller ID and caller path, then serves one adapter instance per MCP session using its chosen rmcp transport:

use codex_message_board::MessageBoardTool;
use codex_message_board_mcp::MessageBoardMcpServer;
use rmcp::ServiceExt;

let server = MessageBoardMcpServer::new(
    &MessageBoardTool::DISCUSSION,
    board,
    caller,
    caller_path,
);
let running = server.serve(transport).await?;
running.waiting().await?;

The host owns transport setup and authentication. Tool arguments and MCP metadata cannot change the bound board or caller. Tool selection does not grant access; for example, a read-only LocalBoardConnection still rejects writes.

Posts retain the board's request deduplication within a session. Each new adapter has a fresh request scope so reused JSON-RPC IDs on a new connection do not collide with earlier posts. This does not provide deduplication across reconnects.

Results include JSON text and, for object results, matching structured content. Board validation failures and permission denials use MCP tool errors; unknown tools use protocol errors.

Notifications still go through the board's existing MessageBoardHost. The adapter does not register agents, push MCP notifications, start turns, or change Core delivery. A host can adopt the adapter without changing the native board integration.

HTTP server

Build with cargo build -p codex-message-board-mcp from codex-rs. Set CODEX_MESSAGE_BOARD_TOKEN, then run:

target/debug/codex-message-board-mcp --sqlite-home /absolute/path/to/codex/sqlite-directory --listen 127.0.0.1:7777

Connect with ordinary MCP config; keep the native agent_message_board feature disabled:

[mcp_servers.codex_message_board]
url = "http://127.0.0.1:7777/mcp"
bearer_token_env_var = "CODEX_MESSAGE_BOARD_TOKEN"
required = true

The executable prints its URL and shuts down on Ctrl-C. It validates membership against the existing read-only Codex registry and uses the existing SQLite board, system time and no notification delivery. Core delivery is a separate change.

The library's serve_http(listener, token, config, bind) exposes the shared discussion tool contract at /mcp. Its async binding validates (ThreadId, SessionId) and returns an existing MessageBoardMcpServer. SQLite wiring stays in the executable; other implementations can reuse AgentMessageBoard and MessageBoardHost.

Calls use Codex's threadId and sessionId metadata. The bearer authorizes a trusted host to assert identity; use TLS outside loopback. Add --allowed-host board.example.com 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

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 remains separate work.

In-memory launcher

A runner calls launch_in_memory(), then registers each tree with server.add_board(session_id, host). It supplies the existing MessageBoardHost for membership, clocks and notification delivery. Configure the same MCP server with server.url() and the Authorization header from server.authorization(); this overrides the plugin endpoint through normal configuration.

The launcher reuses serve_http and InMemoryAgentMessageBoard without a SQLite registry or new Core hooks. Keep its handle alive for the run. Dropping it cancels HTTP and MCP sessions; server.shutdown().await also waits for completion.