Allow parallel MCP tool calls when annotated readOnly (#23750)

## Summary
- Treat MCP tools with `readOnlyHint: true` as parallel-safe even when
`supports_parallel_tool_calls` is unset or `false`.
- Keep server-level `supports_parallel_tool_calls` as an additive
override for non-read-only tools.
- Add focused unit coverage for the MCP handler eligibility decision.
- Update RMCP integration coverage to keep the serial baseline on a
mutable tool, verify read-only concurrency without server opt-in, and
preserve the server opt-in concurrency path separately.

## Testing
- `just fmt`
- `cargo test -p codex-core --lib tools::handlers::mcp::tests::`
- `cargo test -p codex-core --test all
stdio_mcp_read_only_tool_calls_run_concurrently_without_server_opt_in`
- `cargo test -p codex-core --test all
stdio_mcp_parallel_tool_calls_opt_in_runs_concurrently`
- `cargo test -p codex-rmcp-client`
This commit is contained in:
anp-oai
2026-05-21 20:40:34 -07:00
committed by GitHub
Unverified
parent 464ab40dfa
commit c83ba22359
3 changed files with 181 additions and 3 deletions
@@ -70,6 +70,7 @@ impl TestToolServer {
Self::echo_dash_tool(),
Self::cwd_tool(),
Self::sync_tool(),
Self::sync_readonly_tool(),
Self::image_tool(),
Self::image_scenario_tool(),
sandbox_meta_tool,
@@ -205,6 +206,12 @@ impl TestToolServer {
}))
.expect("sync tool output schema should deserialize");
tool.output_schema = Some(Arc::new(output_schema));
tool
}
fn sync_readonly_tool() -> Tool {
let mut tool = Self::sync_tool();
tool.name = Cow::Borrowed("sync_readonly");
tool.annotations = Some(ToolAnnotations::new().read_only(true));
tool
}
@@ -551,6 +558,10 @@ impl ServerHandler for TestToolServer {
let args = Self::parse_call_args::<SyncArgs>(&request, "sync")?;
Self::sync_result(args).await
}
"sync_readonly" => {
let args = Self::parse_call_args::<SyncArgs>(&request, "sync_readonly")?;
Self::sync_result(args).await
}
other => Err(McpError::invalid_params(
format!("unknown tool: {other}"),
None,