mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Add supports_parallel_tool_calls flag to included mcps (#17667)
## Why
For more advanced MCP usage, we want the model to be able to emit
parallel MCP tool calls and have Codex execute eligible ones
concurrently, instead of forcing all MCP calls through the serial block.
The main design choice was where to thread the config. I made this
server-level because parallel safety depends on the MCP server
implementation. Codex reads the flag from `mcp_servers`, threads the
opted-in server names into `ToolRouter`, and checks the parsed
`ToolPayload::Mcp { server, .. }` at execution time. That avoids relying
on model-visible tool names, which can be incomplete in
deferred/search-tool paths or ambiguous for similarly named
servers/tools.
## What was added
Added `supports_parallel_tool_calls` for MCP servers.
Before:
```toml
[mcp_servers.docs]
command = "docs-server"
```
After:
```toml
[mcp_servers.docs]
command = "docs-server"
supports_parallel_tool_calls = true
```
MCP calls remain serial by default. Only tools from opted-in servers are
eligible to run in parallel. Docs also now warn to enable this only when
the server’s tools are safe to run concurrently, especially around
shared state or read/write races.
## Testing
Tested with a local stdio MCP server exposing real delay tools. The
model/Responses side was mocked only to deterministically emit two MCP
calls in the same turn.
Each test called `query_with_delay` and `query_with_delay_2` with `{
"seconds": 25 }`.
| Build/config | Observed | Wall time |
| --- | --- | --- |
| main with flag enabled | serial | `58.79s` |
| PR with flag enabled | parallel | `31.73s` |
| PR without flag | serial | `56.70s` |
PR with flag enabled showed both tools start before either completed;
main and PR-without-flag completed the first delay before starting the
second.
Also added an integration test.
Additional checks:
- `cargo test -p codex-tools` passed
- `cargo test -p codex-core
mcp_parallel_support_uses_exact_payload_server` passed
- `git diff --check` passed
This commit is contained in:
committed by
GitHub
Unverified
parent
0e31dc0d4a
commit
937dd3812d
@@ -7133,11 +7133,24 @@ pub(crate) async fn built_tools(
|
||||
);
|
||||
let direct_mcp_tools = has_mcp_servers.then_some(mcp_tool_exposure.direct_tools);
|
||||
|
||||
let parallel_mcp_server_names = turn_context
|
||||
.config
|
||||
.mcp_servers
|
||||
.get()
|
||||
.iter()
|
||||
.filter_map(|(server_name, server_config)| {
|
||||
server_config
|
||||
.supports_parallel_tool_calls
|
||||
.then_some(server_name.clone())
|
||||
})
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
Ok(Arc::new(ToolRouter::from_config(
|
||||
&turn_context.tools_config,
|
||||
ToolRouterParams {
|
||||
deferred_mcp_tools: mcp_tool_exposure.deferred_tools,
|
||||
mcp_tools: direct_mcp_tools,
|
||||
deferred_mcp_tools: mcp_tool_exposure.deferred_tools,
|
||||
parallel_mcp_server_names,
|
||||
discoverable_tools,
|
||||
dynamic_tools: turn_context.dynamic_tools.as_slice(),
|
||||
},
|
||||
|
||||
@@ -312,6 +312,7 @@ fn test_tool_runtime(session: Arc<Session>, turn_context: Arc<TurnContext>) -> T
|
||||
crate::tools::router::ToolRouterParams {
|
||||
mcp_tools: None,
|
||||
deferred_mcp_tools: None,
|
||||
parallel_mcp_server_names: HashSet::new(),
|
||||
discoverable_tools: None,
|
||||
dynamic_tools: turn_context.dynamic_tools.as_slice(),
|
||||
},
|
||||
@@ -5353,6 +5354,7 @@ async fn fatal_tool_error_stops_turn_and_reports_error() {
|
||||
crate::tools::router::ToolRouterParams {
|
||||
deferred_mcp_tools,
|
||||
mcp_tools: Some(tools),
|
||||
parallel_mcp_server_names: HashSet::new(),
|
||||
discoverable_tools: None,
|
||||
dynamic_tools: turn_context.dynamic_tools.as_slice(),
|
||||
},
|
||||
|
||||
@@ -83,6 +83,7 @@ fn stdio_mcp(command: &str) -> McpServerConfig {
|
||||
},
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
@@ -104,6 +105,7 @@ fn http_mcp(url: &str) -> McpServerConfig {
|
||||
},
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
@@ -2011,6 +2013,7 @@ async fn replace_mcp_servers_round_trips_entries() -> anyhow::Result<()> {
|
||||
},
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: Some(Duration::from_secs(3)),
|
||||
tool_timeout_sec: Some(Duration::from_secs(5)),
|
||||
@@ -2257,6 +2260,7 @@ async fn replace_mcp_servers_serializes_env_sorted() -> anyhow::Result<()> {
|
||||
},
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
@@ -2330,6 +2334,7 @@ async fn replace_mcp_servers_serializes_env_vars() -> anyhow::Result<()> {
|
||||
},
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
@@ -2383,6 +2388,7 @@ async fn replace_mcp_servers_serializes_cwd() -> anyhow::Result<()> {
|
||||
},
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
@@ -2434,6 +2440,7 @@ async fn replace_mcp_servers_streamable_http_serializes_bearer_token() -> anyhow
|
||||
},
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: Some(Duration::from_secs(2)),
|
||||
tool_timeout_sec: None,
|
||||
@@ -2501,6 +2508,7 @@ async fn replace_mcp_servers_streamable_http_serializes_custom_headers() -> anyh
|
||||
},
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: Some(Duration::from_secs(2)),
|
||||
tool_timeout_sec: None,
|
||||
@@ -2580,6 +2588,7 @@ async fn replace_mcp_servers_streamable_http_removes_optional_sections() -> anyh
|
||||
},
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: Some(Duration::from_secs(2)),
|
||||
tool_timeout_sec: None,
|
||||
@@ -2612,6 +2621,7 @@ async fn replace_mcp_servers_streamable_http_removes_optional_sections() -> anyh
|
||||
},
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
@@ -2679,6 +2689,7 @@ async fn replace_mcp_servers_streamable_http_isolates_headers_between_servers()
|
||||
},
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: Some(Duration::from_secs(2)),
|
||||
tool_timeout_sec: None,
|
||||
@@ -2701,6 +2712,7 @@ async fn replace_mcp_servers_streamable_http_isolates_headers_between_servers()
|
||||
},
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
@@ -2786,6 +2798,7 @@ async fn replace_mcp_servers_serializes_disabled_flag() -> anyhow::Result<()> {
|
||||
},
|
||||
enabled: false,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
@@ -2833,6 +2846,7 @@ async fn replace_mcp_servers_serializes_required_flag() -> anyhow::Result<()> {
|
||||
},
|
||||
enabled: true,
|
||||
required: true,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
@@ -2880,6 +2894,7 @@ async fn replace_mcp_servers_serializes_tool_filters() -> anyhow::Result<()> {
|
||||
},
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
@@ -2931,6 +2946,7 @@ async fn replace_mcp_servers_streamable_http_serializes_oauth_resource() -> anyh
|
||||
},
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
|
||||
@@ -225,6 +225,9 @@ mod document_helpers {
|
||||
if config.required {
|
||||
entry["required"] = value(true);
|
||||
}
|
||||
if config.supports_parallel_tool_calls {
|
||||
entry["supports_parallel_tool_calls"] = value(true);
|
||||
}
|
||||
if let Some(timeout) = config.startup_timeout_sec {
|
||||
entry["startup_timeout_sec"] = value(timeout.as_secs_f64());
|
||||
}
|
||||
|
||||
@@ -577,6 +577,7 @@ fn blocking_replace_mcp_servers_round_trips() {
|
||||
},
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: true,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
@@ -603,6 +604,7 @@ fn blocking_replace_mcp_servers_round_trips() {
|
||||
},
|
||||
enabled: false,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: Some(std::time::Duration::from_secs(5)),
|
||||
tool_timeout_sec: None,
|
||||
@@ -638,6 +640,7 @@ Z-Header = \"z\"
|
||||
command = \"cmd\"
|
||||
args = [\"--flag\"]
|
||||
env_vars = [\"FOO\"]
|
||||
supports_parallel_tool_calls = true
|
||||
enabled_tools = [\"one\", \"two\"]
|
||||
|
||||
[mcp_servers.stdio.env]
|
||||
@@ -665,6 +668,7 @@ fn blocking_replace_mcp_servers_serializes_tool_approval_overrides() {
|
||||
},
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
@@ -725,6 +729,7 @@ foo = { command = "cmd" }
|
||||
},
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
@@ -776,6 +781,7 @@ foo = { command = "cmd" } # keep me
|
||||
},
|
||||
enabled: false,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
@@ -826,6 +832,7 @@ foo = { command = "cmd", args = ["--flag"] } # keep me
|
||||
},
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
@@ -877,6 +884,7 @@ foo = { command = "cmd" }
|
||||
},
|
||||
enabled: false,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
|
||||
@@ -364,6 +364,7 @@ fn mcp_dependency_to_server_config(
|
||||
},
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
@@ -390,6 +391,7 @@ fn mcp_dependency_to_server_config(
|
||||
},
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
|
||||
@@ -179,6 +179,7 @@ fn load_plugins_loads_default_skills_and_mcp_servers() {
|
||||
},
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
@@ -510,6 +511,7 @@ fn load_plugins_uses_manifest_configured_component_paths() {
|
||||
},
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
@@ -617,6 +619,7 @@ fn load_plugins_ignores_manifest_component_paths_without_dot_slash() {
|
||||
},
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
@@ -772,6 +775,7 @@ fn capability_index_filters_inactive_and_zero_capability_plugins() {
|
||||
},
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
|
||||
@@ -2,6 +2,7 @@ mod execute_handler;
|
||||
mod response_adapter;
|
||||
mod wait_handler;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
@@ -251,7 +252,7 @@ pub(super) async fn build_enabled_tools(
|
||||
|
||||
async fn build_nested_router(exec: &ExecContext) -> ToolRouter {
|
||||
let nested_tools_config = exec.turn.tools_config.for_code_mode_nested_tools();
|
||||
let mcp_tools = exec
|
||||
let listed_mcp_tools = exec
|
||||
.session
|
||||
.services
|
||||
.mcp_connection_manager
|
||||
@@ -259,12 +260,25 @@ async fn build_nested_router(exec: &ExecContext) -> ToolRouter {
|
||||
.await
|
||||
.list_all_tools()
|
||||
.await;
|
||||
let parallel_mcp_server_names = exec
|
||||
.turn
|
||||
.config
|
||||
.mcp_servers
|
||||
.get()
|
||||
.iter()
|
||||
.filter_map(|(server_name, server_config)| {
|
||||
server_config
|
||||
.supports_parallel_tool_calls
|
||||
.then_some(server_name.clone())
|
||||
})
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
ToolRouter::from_config(
|
||||
&nested_tools_config,
|
||||
ToolRouterParams {
|
||||
deferred_mcp_tools: None,
|
||||
mcp_tools: Some(mcp_tools),
|
||||
mcp_tools: Some(listed_mcp_tools),
|
||||
parallel_mcp_server_names,
|
||||
discoverable_tools: None,
|
||||
dynamic_tools: exec.turn.dynamic_tools.as_slice(),
|
||||
},
|
||||
|
||||
@@ -1561,12 +1561,14 @@ impl JsReplManager {
|
||||
.await
|
||||
.list_all_tools()
|
||||
.await;
|
||||
|
||||
let router = ToolRouter::from_config(
|
||||
&exec.turn.tools_config,
|
||||
crate::tools::router::ToolRouterParams {
|
||||
deferred_mcp_tools: None,
|
||||
mcp_tools: Some(mcp_tools),
|
||||
// JS REPL dispatches nested tool calls directly, not through
|
||||
// `ToolCallRuntime`'s parallel scheduling lock.
|
||||
parallel_mcp_server_names: std::collections::HashSet::new(),
|
||||
discoverable_tools: None,
|
||||
dynamic_tools: exec.turn.dynamic_tools.as_slice(),
|
||||
},
|
||||
|
||||
@@ -78,7 +78,7 @@ impl ToolCallRuntime {
|
||||
source: ToolCallSource,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> impl std::future::Future<Output = Result<AnyToolResult, FunctionCallError>> {
|
||||
let supports_parallel = self.router.tool_supports_parallel(&call.tool_name);
|
||||
let supports_parallel = self.router.tool_supports_parallel(&call);
|
||||
let router = Arc::clone(&self.router);
|
||||
let session = Arc::clone(&self.session);
|
||||
let turn = Arc::clone(&self.turn_context);
|
||||
|
||||
@@ -20,6 +20,7 @@ use codex_tools::ToolName;
|
||||
use codex_tools::ToolSpec;
|
||||
use codex_tools::ToolsConfig;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
use tracing::instrument;
|
||||
|
||||
@@ -36,11 +37,13 @@ pub struct ToolRouter {
|
||||
registry: ToolRegistry,
|
||||
specs: Vec<ConfiguredToolSpec>,
|
||||
model_visible_specs: Vec<ToolSpec>,
|
||||
parallel_mcp_server_names: HashSet<String>,
|
||||
}
|
||||
|
||||
pub(crate) struct ToolRouterParams<'a> {
|
||||
pub(crate) mcp_tools: Option<HashMap<String, ToolInfo>>,
|
||||
pub(crate) deferred_mcp_tools: Option<HashMap<String, ToolInfo>>,
|
||||
pub(crate) parallel_mcp_server_names: HashSet<String>,
|
||||
pub(crate) discoverable_tools: Option<Vec<DiscoverableTool>>,
|
||||
pub(crate) dynamic_tools: &'a [DynamicToolSpec],
|
||||
}
|
||||
@@ -50,6 +53,7 @@ impl ToolRouter {
|
||||
let ToolRouterParams {
|
||||
mcp_tools,
|
||||
deferred_mcp_tools,
|
||||
parallel_mcp_server_names,
|
||||
discoverable_tools,
|
||||
dynamic_tools,
|
||||
} = params;
|
||||
@@ -83,6 +87,7 @@ impl ToolRouter {
|
||||
registry,
|
||||
specs,
|
||||
model_visible_specs,
|
||||
parallel_mcp_server_names,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +109,7 @@ impl ToolRouter {
|
||||
.map(|config| config.spec.clone())
|
||||
}
|
||||
|
||||
pub fn tool_supports_parallel(&self, tool_name: &ToolName) -> bool {
|
||||
fn configured_tool_supports_parallel(&self, tool_name: &ToolName) -> bool {
|
||||
tool_name.namespace.is_none()
|
||||
&& self
|
||||
.specs
|
||||
@@ -113,6 +118,16 @@ impl ToolRouter {
|
||||
.any(|config| config.name() == tool_name.name.as_str())
|
||||
}
|
||||
|
||||
pub fn tool_supports_parallel(&self, call: &ToolCall) -> bool {
|
||||
match &call.payload {
|
||||
// MCP parallel support is configured per server, including for deferred
|
||||
// tools that may not have a matching spec entry. Use the parsed payload
|
||||
// server so similarly named servers/tools cannot collide.
|
||||
ToolPayload::Mcp { server, .. } => self.parallel_mcp_server_names.contains(server),
|
||||
_ => self.configured_tool_supports_parallel(&call.tool_name),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip_all, err)]
|
||||
pub async fn build_tool_call(
|
||||
session: &Session,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::codex::make_session_and_context;
|
||||
@@ -32,6 +33,7 @@ async fn js_repl_tools_only_blocks_direct_tool_calls() -> anyhow::Result<()> {
|
||||
ToolRouterParams {
|
||||
deferred_mcp_tools,
|
||||
mcp_tools: Some(mcp_tools),
|
||||
parallel_mcp_server_names: HashSet::new(),
|
||||
discoverable_tools: None,
|
||||
dynamic_tools: turn.dynamic_tools.as_slice(),
|
||||
},
|
||||
@@ -84,6 +86,7 @@ async fn js_repl_tools_only_allows_js_repl_source_calls() -> anyhow::Result<()>
|
||||
ToolRouterParams {
|
||||
deferred_mcp_tools,
|
||||
mcp_tools: Some(mcp_tools),
|
||||
parallel_mcp_server_names: HashSet::new(),
|
||||
discoverable_tools: None,
|
||||
dynamic_tools: turn.dynamic_tools.as_slice(),
|
||||
},
|
||||
@@ -129,6 +132,7 @@ async fn js_repl_tools_only_blocks_namespaced_js_repl_tool() -> anyhow::Result<(
|
||||
ToolRouterParams {
|
||||
deferred_mcp_tools: None,
|
||||
mcp_tools: None,
|
||||
parallel_mcp_server_names: HashSet::new(),
|
||||
discoverable_tools: None,
|
||||
dynamic_tools: turn.dynamic_tools.as_slice(),
|
||||
},
|
||||
@@ -178,6 +182,7 @@ async fn parallel_support_does_not_match_namespaced_local_tool_names() -> anyhow
|
||||
ToolRouterParams {
|
||||
deferred_mcp_tools: None,
|
||||
mcp_tools: Some(mcp_tools),
|
||||
parallel_mcp_server_names: HashSet::new(),
|
||||
discoverable_tools: None,
|
||||
dynamic_tools: turn.dynamic_tools.as_slice(),
|
||||
},
|
||||
@@ -185,12 +190,24 @@ async fn parallel_support_does_not_match_namespaced_local_tool_names() -> anyhow
|
||||
|
||||
let parallel_tool_name = ["shell", "local_shell", "exec_command", "shell_command"]
|
||||
.into_iter()
|
||||
.find(|name| router.tool_supports_parallel(&ToolName::plain(*name)))
|
||||
.find(|name| {
|
||||
router.tool_supports_parallel(&ToolCall {
|
||||
tool_name: ToolName::plain(*name),
|
||||
call_id: "call-parallel-tool".to_string(),
|
||||
payload: ToolPayload::Function {
|
||||
arguments: "{}".to_string(),
|
||||
},
|
||||
})
|
||||
})
|
||||
.expect("test session should expose a parallel shell-like tool");
|
||||
|
||||
assert!(
|
||||
!router.tool_supports_parallel(&ToolName::namespaced("mcp__server__", parallel_tool_name))
|
||||
);
|
||||
assert!(!router.tool_supports_parallel(&ToolCall {
|
||||
tool_name: ToolName::namespaced("mcp__server__", parallel_tool_name),
|
||||
call_id: "call-namespaced-tool".to_string(),
|
||||
payload: ToolPayload::Function {
|
||||
arguments: "{}".to_string(),
|
||||
},
|
||||
}));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -228,3 +245,42 @@ async fn build_tool_call_uses_namespace_for_registry_name() -> anyhow::Result<()
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mcp_parallel_support_uses_exact_payload_server() -> anyhow::Result<()> {
|
||||
let (_, turn) = make_session_and_context().await;
|
||||
let router = ToolRouter::from_config(
|
||||
&turn.tools_config,
|
||||
ToolRouterParams {
|
||||
deferred_mcp_tools: None,
|
||||
mcp_tools: None,
|
||||
parallel_mcp_server_names: HashSet::from(["echo".to_string()]),
|
||||
discoverable_tools: None,
|
||||
dynamic_tools: turn.dynamic_tools.as_slice(),
|
||||
},
|
||||
);
|
||||
|
||||
let deferred_call = ToolCall {
|
||||
tool_name: ToolName::namespaced("mcp__echo__", "query_with_delay"),
|
||||
call_id: "call-deferred".to_string(),
|
||||
payload: ToolPayload::Mcp {
|
||||
server: "echo".to_string(),
|
||||
tool: "query_with_delay".to_string(),
|
||||
raw_arguments: "{}".to_string(),
|
||||
},
|
||||
};
|
||||
assert!(router.tool_supports_parallel(&deferred_call));
|
||||
|
||||
let different_server_call = ToolCall {
|
||||
tool_name: ToolName::namespaced("mcp__hello_echo__", "query_with_delay"),
|
||||
call_id: "call-other-server".to_string(),
|
||||
payload: ToolPayload::Mcp {
|
||||
server: "hello_echo".to_string(),
|
||||
tool: "query_with_delay".to_string(),
|
||||
raw_arguments: "{}".to_string(),
|
||||
},
|
||||
};
|
||||
assert!(!router.tool_supports_parallel(&different_server_call));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -314,6 +314,7 @@ fn assert_model_tools(
|
||||
ToolRouterParams {
|
||||
mcp_tools: None,
|
||||
deferred_mcp_tools: None,
|
||||
parallel_mcp_server_names: std::collections::HashSet::new(),
|
||||
discoverable_tools: None,
|
||||
dynamic_tools: &[],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user