feat: add supports_parallel_tool_calls for MCP servers (#26825)

Port from openai/codex#17667: MCP servers can now opt-in to parallel
tool execution by setting supports_parallel_tool_calls: true in their
config. This allows tools from the same server to run concurrently
within a single tool-call batch, matching the behavior already available
for built-in tools like web_search and read_file.

Previously all MCP tools were forced sequential because they weren't in
the _PARALLEL_SAFE_TOOLS set. Now _should_parallelize_tool_batch checks
is_mcp_tool_parallel_safe() which looks up the server's config flag.

Config example:
  mcp_servers:
    docs:
      command: "docs-server"
      supports_parallel_tool_calls: true

Changes:
- tools/mcp_tool.py: Track parallel-safe servers in _parallel_safe_servers
  set, populated during register_mcp_servers(). Add is_mcp_tool_parallel_safe()
  public API.
- run_agent.py: Add _is_mcp_tool_parallel_safe() lazy-import wrapper. Update
  _should_parallelize_tool_batch() to check MCP tools against server config.
- 11 new tests covering the feature end-to-end.
- Updated MCP docs and config reference.
This commit is contained in:
Teknium 2026-05-16 01:04:28 -07:00 committed by GitHub
parent c445f48b78
commit 395e9dd9e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 260 additions and 2 deletions

View file

@ -3762,3 +3762,135 @@ class TestRegisterMcpServers:
)
_servers.pop("srv", None)
# ---------------------------------------------------------------------------
# Tests for parallel tool call support (port from openai/codex#17667)
# ---------------------------------------------------------------------------
class TestMcpParallelToolCalls:
"""Tests for the supports_parallel_tool_calls config option."""
def test_is_mcp_tool_parallel_safe_non_mcp_tool(self):
"""Non-MCP tool names always return False."""
from tools.mcp_tool import is_mcp_tool_parallel_safe
assert is_mcp_tool_parallel_safe("web_search") is False
assert is_mcp_tool_parallel_safe("read_file") is False
assert is_mcp_tool_parallel_safe("terminal") is False
assert is_mcp_tool_parallel_safe("") is False
def test_is_mcp_tool_parallel_safe_no_servers(self):
"""MCP tool from unknown server returns False."""
from tools.mcp_tool import is_mcp_tool_parallel_safe, _parallel_safe_servers, _lock
with _lock:
_parallel_safe_servers.clear()
assert is_mcp_tool_parallel_safe("mcp_docs_search") is False
def test_is_mcp_tool_parallel_safe_with_flag(self):
"""MCP tool from a parallel-safe server returns True."""
from tools.mcp_tool import is_mcp_tool_parallel_safe, _parallel_safe_servers, _lock
with _lock:
_parallel_safe_servers.add("docs")
try:
assert is_mcp_tool_parallel_safe("mcp_docs_search") is True
assert is_mcp_tool_parallel_safe("mcp_docs_read_file") is True
# Different server should be False
assert is_mcp_tool_parallel_safe("mcp_github_list_repos") is False
finally:
with _lock:
_parallel_safe_servers.discard("docs")
def test_is_mcp_tool_parallel_safe_server_with_underscores(self):
"""Server names containing underscores are correctly matched."""
from tools.mcp_tool import is_mcp_tool_parallel_safe, _parallel_safe_servers, _lock
with _lock:
_parallel_safe_servers.add("my_server")
try:
assert is_mcp_tool_parallel_safe("mcp_my_server_query") is True
finally:
with _lock:
_parallel_safe_servers.discard("my_server")
def test_is_mcp_tool_parallel_safe_no_tool_suffix(self):
"""Tool name that is just 'mcp_{server}' without a tool part returns False."""
from tools.mcp_tool import is_mcp_tool_parallel_safe, _parallel_safe_servers, _lock
with _lock:
_parallel_safe_servers.add("docs")
try:
# "mcp_docs" has no tool part after the server name
assert is_mcp_tool_parallel_safe("mcp_docs") is False
# "mcp_docs_" has empty tool part
assert is_mcp_tool_parallel_safe("mcp_docs_") is False
finally:
with _lock:
_parallel_safe_servers.discard("docs")
def test_register_mcp_servers_tracks_parallel_flag(self):
"""register_mcp_servers populates _parallel_safe_servers from config."""
from tools.mcp_tool import (
register_mcp_servers, _parallel_safe_servers, _lock,
sanitize_mcp_name_component,
)
fake_config = {
"parallel_srv": {
"command": "echo",
"supports_parallel_tool_calls": True,
},
"serial_srv": {
"command": "echo",
"supports_parallel_tool_calls": False,
},
"default_srv": {
"command": "echo",
# no supports_parallel_tool_calls key
},
}
with patch("tools.mcp_tool._MCP_AVAILABLE", True), \
patch("tools.mcp_tool._ensure_mcp_loop"), \
patch("tools.mcp_tool._run_on_mcp_loop"), \
patch("tools.mcp_tool._existing_tool_names", return_value=[]):
register_mcp_servers(fake_config)
with _lock:
assert sanitize_mcp_name_component("parallel_srv") in _parallel_safe_servers
assert sanitize_mcp_name_component("serial_srv") not in _parallel_safe_servers
assert sanitize_mcp_name_component("default_srv") not in _parallel_safe_servers
# Cleanup
_parallel_safe_servers.discard(sanitize_mcp_name_component("parallel_srv"))
def test_register_mcp_servers_removes_parallel_flag_on_toggle(self):
"""Toggling supports_parallel_tool_calls to false removes server from the set."""
from tools.mcp_tool import (
register_mcp_servers, _parallel_safe_servers, _lock,
sanitize_mcp_name_component,
)
# First registration: parallel enabled
config_on = {
"toggle_srv": {
"command": "echo",
"supports_parallel_tool_calls": True,
},
}
with patch("tools.mcp_tool._MCP_AVAILABLE", True), \
patch("tools.mcp_tool._ensure_mcp_loop"), \
patch("tools.mcp_tool._run_on_mcp_loop"), \
patch("tools.mcp_tool._existing_tool_names", return_value=[]):
register_mcp_servers(config_on)
with _lock:
assert sanitize_mcp_name_component("toggle_srv") in _parallel_safe_servers
# Second registration: parallel disabled
config_off = {
"toggle_srv": {
"command": "echo",
"supports_parallel_tool_calls": False,
},
}
with patch("tools.mcp_tool._MCP_AVAILABLE", True), \
patch("tools.mcp_tool._ensure_mcp_loop"), \
patch("tools.mcp_tool._run_on_mcp_loop"), \
patch("tools.mcp_tool._existing_tool_names", return_value=[]):
register_mcp_servers(config_off)
with _lock:
assert sanitize_mcp_name_component("toggle_srv") not in _parallel_safe_servers