Simplify MCP tool handler plumbing (#21595)

## Why
The MCP tool path had accumulated a few core-owned special cases: a
dedicated payload variant, resolver plumbing, a legacy `AfterToolUse`
translation path, and a side channel for parallel-call metadata. That
made `ToolRegistry` and the spec builder know more about MCP than they
needed to.

This change moves MCP-specific execution details back onto `ToolInfo`
and `McpHandler` so `codex-core` can treat MCP calls like normal
function calls while still preserving MCP-specific dispatch and
telemetry behavior where it belongs.

## What changed
- removed `resolve_mcp_tool_info`, `ToolPayload::Mcp`, `ToolKind`, and
the remaining registry-side MCP resolver path
- stored MCP routing metadata directly on `McpHandler` and `ToolInfo`,
including `supports_parallel_tool_calls`
- deleted the legacy `AfterToolUse` consumer in `core`, which removes
the need for handler-specific `after_tool_use_payload` implementations
- switched tool-result telemetry to handler-provided tags and kept
MCP-specific dispatch payload construction inside the handler
- simplified tool spec planning/building by passing `ToolInfo` directly
and dropping the direct/deferred MCP wrapper structs and the
parallel-server side table

## Testing
- `cargo check -p codex-core -p codex-mcp -p codex-otel`
- `cargo test -p codex-core
mcp_parallel_support_uses_exact_payload_server`
- `cargo test -p codex-core
direct_mcp_tools_register_namespaced_handlers`
- `cargo test -p codex-core
search_tool_description_lists_each_mcp_source_once`
- `cargo test -p codex-mcp
list_all_tools_uses_startup_snapshot_while_client_is_pending`
- `just fix -p codex-core -p codex-mcp -p codex-otel`
This commit is contained in:
pakrym-oai
2026-05-11 17:11:31 -07:00
committed by GitHub
Unverified
parent e16b4e46d4
commit ed5944ba1d
70 changed files with 412 additions and 638 deletions
+21 -22
View File
@@ -7,7 +7,6 @@
//! `codex-core`.
use std::collections::HashMap;
use std::collections::HashSet;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
@@ -44,7 +43,6 @@ use codex_config::Constrained;
use codex_config::McpServerTransportConfig;
use codex_config::types::OAuthCredentialsStoreMode;
use codex_login::CodexAuth;
use codex_protocol::ToolName;
use codex_protocol::mcp::CallToolResult;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
@@ -131,17 +129,6 @@ impl McpConnectionManager {
.is_none_or(|metadata| metadata.pollutes_memory)
}
pub fn parallel_tool_call_server_names(&self) -> HashSet<String> {
self.server_metadata
.iter()
.filter_map(|(name, metadata)| {
metadata
.supports_parallel_tool_calls
.then_some(name.clone())
})
.collect()
}
pub fn is_host_owned_codex_apps_server(&self, server_name: &str) -> bool {
self.host_owned_codex_apps_enabled && server_name == CODEX_APPS_MCP_SERVER_NAME
}
@@ -373,7 +360,11 @@ impl McpConnectionManager {
let Some(server_tools) = managed_client.listed_tools().await else {
continue;
};
tools.extend(server_tools);
tools.extend(
server_tools
.into_iter()
.map(|tool| self.with_server_metadata(tool)),
);
}
normalize_tools_for_model(tools)
}
@@ -424,11 +415,26 @@ impl McpConnectionManager {
.into_iter()
.map(|mut tool| {
tool.tool = tool_with_model_visible_input_schema(&tool.tool);
tool
self.with_server_metadata(tool)
});
Ok(normalize_tools_for_model(tools))
}
fn with_server_metadata(&self, mut tool: ToolInfo) -> ToolInfo {
let Some(metadata) = self.server_metadata.get(&tool.server_name) else {
tool.supports_parallel_tool_calls = false;
tool.server_origin = None;
return tool;
};
tool.supports_parallel_tool_calls = metadata.supports_parallel_tool_calls;
tool.server_origin = metadata
.origin
.as_ref()
.map(|origin| origin.as_str().to_string());
tool
}
/// Returns a single map that contains all resources. Each key is the
/// server name and the value is a vector of resources.
pub async fn list_all_resources(&self) -> HashMap<String, Vec<Resource>> {
@@ -662,13 +668,6 @@ impl McpConnectionManager {
.with_context(|| format!("resources/read failed for `{server}` ({uri})"))
}
pub async fn resolve_tool_info(&self, tool_name: &ToolName) -> Option<ToolInfo> {
let all_tools = self.list_all_tools().await;
all_tools
.into_iter()
.find(|tool| tool.canonical_tool_name() == *tool_name)
}
async fn client_by_name(&self, name: &str) -> Result<ManagedClient> {
self.clients
.get(name)
@@ -10,6 +10,7 @@ use crate::elicitation::elicitation_is_rejected_by_policy;
use crate::rmcp_client::AsyncManagedClient;
use crate::rmcp_client::ManagedClient;
use crate::rmcp_client::StartupOutcomeError;
use crate::server::McpServerOrigin;
use crate::tools::ToolFilter;
use crate::tools::ToolInfo;
use crate::tools::filter_tools;
@@ -38,6 +39,8 @@ fn create_test_tool(server_name: &str, tool_name: &str) -> ToolInfo {
let tool_namespace = format!("mcp__{server_name}__");
ToolInfo {
server_name: server_name.to_string(),
supports_parallel_tool_calls: false,
server_origin: None,
callable_name: tool_name.to_string(),
callable_namespace: tool_namespace,
namespace_description: None,
@@ -712,44 +715,6 @@ async fn list_all_tools_uses_startup_snapshot_while_client_is_pending() {
assert_eq!(tool.callable_name, "calendar_create_event");
}
#[tokio::test]
async fn resolve_tool_info_accepts_canonical_namespaced_tool_names() {
let startup_tools = vec![create_test_tool("rmcp", "echo")];
let pending_client = futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
.boxed()
.shared();
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
let permission_profile = Constrained::allow_any(PermissionProfile::default());
let mut manager =
McpConnectionManager::new_uninitialized(&approval_policy, &permission_profile);
manager.clients.insert(
"rmcp".to_string(),
AsyncManagedClient {
client: pending_client,
startup_snapshot: Some(startup_tools),
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
cancel_token: CancellationToken::new(),
},
);
let tool = manager
.resolve_tool_info(&ToolName::namespaced("mcp__rmcp__", "echo"))
.await
.expect("split MCP tool namespace and name should resolve");
let expected = ("rmcp", "mcp__rmcp__", "echo", "echo");
assert_eq!(
(
tool.server_name.as_str(),
tool.callable_namespace.as_str(),
tool.callable_name.as_str(),
tool.tool.name.as_ref(),
),
expected
);
}
#[tokio::test]
async fn list_all_tools_blocks_while_client_is_pending_without_startup_snapshot() {
let pending_client = futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
@@ -842,6 +807,46 @@ async fn list_all_tools_uses_startup_snapshot_when_client_startup_fails() {
assert_eq!(tool.callable_name, "calendar_create_event");
}
#[tokio::test]
async fn list_all_tools_adds_server_metadata_to_cached_tools() {
let server_name = "docs";
let startup_tools = vec![create_test_tool(server_name, "search")];
let pending_client = futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
.boxed()
.shared();
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
let permission_profile = Constrained::allow_any(PermissionProfile::default());
let mut manager =
McpConnectionManager::new_uninitialized(&approval_policy, &permission_profile);
manager.server_metadata.insert(
server_name.to_string(),
McpServerMetadata {
pollutes_memory: true,
origin: Some(McpServerOrigin::StreamableHttp(
"https://docs.example".to_string(),
)),
supports_parallel_tool_calls: true,
},
);
manager.clients.insert(
server_name.to_string(),
AsyncManagedClient {
client: pending_client,
startup_snapshot: Some(startup_tools),
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
cancel_token: CancellationToken::new(),
},
);
let tools = manager.list_all_tools().await;
assert_eq!(tools.len(), 1);
let tool = &tools[0];
assert_eq!(tool.server_name, server_name);
assert!(tool.supports_parallel_tool_calls);
assert_eq!(tool.server_origin.as_deref(), Some("https://docs.example"));
}
#[test]
fn elicitation_capability_uses_2025_06_18_shape_for_form_only_support() {
let capability = Some(ElicitationCapability::default());
+2
View File
@@ -375,6 +375,8 @@ pub(crate) async fn list_tools_for_client_uncached(
};
ToolInfo {
server_name: server_name.to_owned(),
supports_parallel_tool_calls: false,
server_origin: None,
callable_name,
callable_namespace,
namespace_description,
+6
View File
@@ -29,6 +29,12 @@ pub(crate) const MCP_TOOLS_CACHE_WRITE_DURATION_METRIC: &str =
pub struct ToolInfo {
/// Raw MCP server name used for routing the tool call.
pub server_name: String,
/// Whether calls routed to this server may run in parallel.
#[serde(default)]
pub supports_parallel_tool_calls: bool,
/// MCP server origin used for telemetry and diagnostics, when known.
#[serde(default)]
pub server_origin: Option<String>,
/// Model-visible tool name used in Responses API tool declarations.
#[serde(rename = "tool_name", alias = "callable_name")]
pub callable_name: String,
+6
View File
@@ -120,6 +120,8 @@ fn codex_app_tool(
ToolInfo {
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
supports_parallel_tool_calls: false,
server_origin: None,
callable_name: tool_name.to_string(),
callable_namespace: tool_namespace,
namespace_description: None,
@@ -190,6 +192,8 @@ fn accessible_connectors_from_mcp_tools_carries_plugin_display_names() {
),
ToolInfo {
server_name: "sample".to_string(),
supports_parallel_tool_calls: false,
server_origin: None,
callable_name: "echo".to_string(),
callable_namespace: "sample".to_string(),
namespace_description: None,
@@ -305,6 +309,8 @@ fn merge_connectors_unions_and_dedupes_plugin_display_names() {
fn accessible_connectors_from_mcp_tools_preserves_description() {
let mcp_tools = vec![ToolInfo {
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
supports_parallel_tool_calls: false,
server_origin: None,
callable_name: "calendar_create_event".to_string(),
callable_namespace: "mcp__codex_apps__calendar".to_string(),
namespace_description: Some("Plan events".to_string()),
@@ -49,6 +49,8 @@ fn make_mcp_tool(
) -> ToolInfo {
ToolInfo {
server_name: server_name.to_string(),
supports_parallel_tool_calls: false,
server_origin: None,
callable_name: callable_name.to_string(),
callable_namespace: callable_namespace.to_string(),
namespace_description: None,
-13
View File
@@ -263,19 +263,6 @@ impl Session {
.await
}
#[expect(
clippy::await_holding_invalid_type,
reason = "MCP tool metadata reads through the session-owned manager guard"
)]
pub(crate) async fn resolve_mcp_tool_info(&self, tool_name: &ToolName) -> Option<ToolInfo> {
self.services
.mcp_connection_manager
.read()
.await
.resolve_tool_info(tool_name)
.await
}
async fn refresh_mcp_servers_inner(
&self,
turn_context: &TurnContext,
-2
View File
@@ -64,7 +64,6 @@ use codex_login::auth_env_telemetry::collect_auth_env_telemetry;
use codex_login::default_client::originator;
use codex_mcp::McpConnectionManager;
use codex_mcp::McpRuntimeEnvironment;
use codex_mcp::ToolInfo;
use codex_mcp::codex_apps_tools_cache_key;
use codex_models_manager::manager::RefreshStrategy;
use codex_models_manager::manager::SharedModelsManager;
@@ -75,7 +74,6 @@ use codex_otel::current_span_trace_id;
use codex_otel::current_span_w3c_trace_context;
use codex_otel::set_parent_from_w3c_trace_context;
use codex_protocol::ThreadId;
use codex_protocol::ToolName;
use codex_protocol::account::PlanType as AccountPlanType;
use codex_protocol::approvals::ElicitationRequestEvent;
use codex_protocol::approvals::ExecPolicyAmendment;
+1 -4
View File
@@ -543,7 +543,6 @@ fn test_tool_runtime(session: Arc<Session>, turn_context: Arc<TurnContext>) -> T
mcp_tools: None,
deferred_mcp_tools: None,
unavailable_called_tools: Vec::new(),
parallel_mcp_server_names: HashSet::new(),
discoverable_tools: None,
extension_tool_bundles: Vec::new(),
dynamic_tools: turn_context.dynamic_tools.as_slice(),
@@ -8641,7 +8640,6 @@ async fn fatal_tool_error_stops_turn_and_reports_error() {
deferred_mcp_tools,
mcp_tools: Some(tools),
unavailable_called_tools: Vec::new(),
parallel_mcp_server_names: HashSet::new(),
discoverable_tools: None,
extension_tool_bundles: Vec::new(),
dynamic_tools: turn_context.dynamic_tools.as_slice(),
@@ -8655,8 +8653,7 @@ async fn fatal_tool_error_stops_turn_and_reports_error() {
input: "{}".to_string(),
};
let call = ToolRouter::build_tool_call(session.as_ref(), item.clone())
.await
let call = ToolRouter::build_tool_call(item.clone())
.expect("build tool call")
.expect("tool call present");
let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new()));
-2
View File
@@ -1161,7 +1161,6 @@ pub(crate) async fn built_tools(
.list_all_tools()
.or_cancel(cancellation_token)
.await?;
let parallel_mcp_server_names = mcp_connection_manager.parallel_tool_call_server_names();
drop(mcp_connection_manager);
let loaded_plugins = sess
.services
@@ -1267,7 +1266,6 @@ pub(crate) async fn built_tools(
mcp_tools,
deferred_mcp_tools,
unavailable_called_tools,
parallel_mcp_server_names,
discoverable_tools,
extension_tool_bundles: extension_tool_bundles(sess),
dynamic_tools: turn_context.dynamic_tools.as_slice(),
+1 -1
View File
@@ -228,7 +228,7 @@ pub(crate) async fn handle_output_item_done(
let mut output = OutputItemResult::default();
let plan_mode = ctx.turn_context.collaboration_mode.mode == ModeKind::Plan;
match ToolRouter::build_tool_call(ctx.sess.as_ref(), item.clone()).await {
match ToolRouter::build_tool_call(item.clone()) {
// The model emitted a tool call; log it, persist the item immediately, and queue the tool execution.
Ok(Some(call)) => {
ctx.sess
@@ -3,7 +3,6 @@ use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
@@ -94,10 +93,6 @@ impl ToolHandler for CodeModeExecuteHandler {
Some(self.spec.clone())
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Custom { .. })
}
+6 -23
View File
@@ -276,7 +276,6 @@ async fn build_nested_router(exec: &ExecContext) -> ToolRouter {
let nested_tools_config = exec.turn.tools_config.for_code_mode_nested_tools();
let mcp_connection_manager = exec.session.services.mcp_connection_manager.read().await;
let listed_mcp_tools = mcp_connection_manager.list_all_tools().await;
let parallel_mcp_server_names = mcp_connection_manager.parallel_tool_call_server_names();
ToolRouter::from_config(
&nested_tools_config,
@@ -284,7 +283,6 @@ async fn build_nested_router(exec: &ExecContext) -> ToolRouter {
deferred_mcp_tools: None,
mcp_tools: Some(listed_mcp_tools),
unavailable_called_tools: Vec::new(),
parallel_mcp_server_names,
discoverable_tools: None,
extension_tool_bundles: extension_tool_bundles(exec.session.as_ref()),
dynamic_tools: exec.turn.dynamic_tools.as_slice(),
@@ -293,7 +291,7 @@ async fn build_nested_router(exec: &ExecContext) -> ToolRouter {
}
async fn call_nested_tool(
exec: ExecContext,
_exec: ExecContext,
tool_runtime: ToolCallRuntime,
invocation: CodeModeNestedToolCall,
cancellation_token: CancellationToken,
@@ -310,29 +308,14 @@ async fn call_nested_tool(
)));
}
let (tool_call_name, payload) =
if let Some(tool_info) = exec.session.resolve_mcp_tool_info(&tool_name).await {
let raw_arguments = match serialize_function_tool_arguments(&tool_name, input) {
Ok(raw_arguments) => raw_arguments,
Err(error) => return Err(FunctionCallError::RespondToModel(error)),
};
(
tool_info.canonical_tool_name(),
ToolPayload::Mcp {
server: tool_info.server_name,
tool: tool_info.tool.name.to_string(),
raw_arguments,
},
)
} else {
match build_nested_tool_payload(tool_runtime.find_spec(&tool_name), &tool_name, input) {
Ok(payload) => (tool_name, payload),
Err(error) => return Err(FunctionCallError::RespondToModel(error)),
}
let payload =
match build_nested_tool_payload(tool_runtime.find_spec(&tool_name), &tool_name, input) {
Ok(payload) => payload,
Err(error) => return Err(FunctionCallError::RespondToModel(error)),
};
let call = ToolCall {
tool_name: tool_call_name,
tool_name,
call_id: format!("{PUBLIC_TOOL_NAME}-{}", uuid::Uuid::new_v4()),
payload,
};
@@ -5,7 +5,6 @@ use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
@@ -52,10 +51,6 @@ impl ToolHandler for CodeModeWaitHandler {
Some(create_wait_tool())
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session,
+6 -26
View File
@@ -58,23 +58,10 @@ pub struct ToolInvocation {
#[derive(Clone, Debug)]
pub enum ToolPayload {
Function {
arguments: String,
},
ToolSearch {
arguments: SearchToolCallParams,
},
Custom {
input: String,
},
LocalShell {
params: ShellToolCallParams,
},
Mcp {
server: String,
tool: String,
raw_arguments: String,
},
Function { arguments: String },
ToolSearch { arguments: SearchToolCallParams },
Custom { input: String },
LocalShell { params: ShellToolCallParams },
}
impl ToolPayload {
@@ -84,7 +71,6 @@ impl ToolPayload {
ToolPayload::ToolSearch { arguments } => Cow::Owned(arguments.query.clone()),
ToolPayload::Custom { input } => Cow::Borrowed(input),
ToolPayload::LocalShell { params } => Cow::Owned(params.command.join(" ")),
ToolPayload::Mcp { raw_arguments, .. } => Cow::Borrowed(raw_arguments),
}
}
}
@@ -363,10 +349,6 @@ impl ToolOutput for AbortedToolOutput {
execution: "client".to_string(),
tools: Vec::new(),
},
ToolPayload::Mcp { .. } => ResponseInputItem::McpToolCallOutput {
call_id: call_id.to_string(),
output: CallToolResult::from_error_text(self.message.clone()),
},
_ => function_tool_response(
call_id,
payload,
@@ -515,10 +497,8 @@ pub(crate) fn response_input_to_code_mode_result(response: ResponseInputItem) ->
},
ResponseInputItem::ToolSearchOutput { tools, .. } => JsonValue::Array(tools),
ResponseInputItem::McpToolCallOutput { output, .. } => {
output.code_mode_result(&ToolPayload::Mcp {
server: String::new(),
tool: String::new(),
raw_arguments: String::new(),
output.code_mode_result(&ToolPayload::Function {
arguments: String::new(),
})
}
}
+10 -20
View File
@@ -61,10 +61,8 @@ fn mcp_code_mode_result_serializes_full_call_tool_result() {
})),
};
let result = output.code_mode_result(&ToolPayload::Mcp {
server: "server".to_string(),
tool: "tool".to_string(),
raw_arguments: "{}".to_string(),
let result = output.code_mode_result(&ToolPayload::Function {
arguments: "{}".to_string(),
});
assert_eq!(
@@ -106,10 +104,8 @@ fn mcp_tool_output_response_item_includes_wall_time() {
let response = output.to_response_item(
"mcp-call-1",
&ToolPayload::Mcp {
server: "server".to_string(),
tool: "tool".to_string(),
raw_arguments: "{}".to_string(),
&ToolPayload::Function {
arguments: "{}".to_string(),
},
);
@@ -160,10 +156,8 @@ fn mcp_tool_output_response_item_truncates_large_structured_content() {
let response = output.to_response_item(
"mcp-call-large",
&ToolPayload::Mcp {
server: "server".to_string(),
tool: "tool".to_string(),
raw_arguments: "{}".to_string(),
&ToolPayload::Function {
arguments: "{}".to_string(),
},
);
@@ -205,10 +199,8 @@ fn mcp_tool_output_response_item_preserves_content_items() {
let response = output.to_response_item(
"mcp-call-2",
&ToolPayload::Mcp {
server: "server".to_string(),
tool: "tool".to_string(),
raw_arguments: "{}".to_string(),
&ToolPayload::Function {
arguments: "{}".to_string(),
},
);
@@ -259,10 +251,8 @@ fn mcp_tool_output_code_mode_result_stays_raw_call_tool_result() {
truncation_policy: TruncationPolicy::Bytes(64),
};
let result = output.code_mode_result(&ToolPayload::Mcp {
server: "server".to_string(),
tool: "tool".to_string(),
raw_arguments: "{}".to_string(),
let result = output.code_mode_result(&ToolPayload::Function {
arguments: "{}".to_string(),
});
assert_eq!(
@@ -4,7 +4,6 @@ use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::agent_jobs_spec::create_report_agent_job_result_tool;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
@@ -23,10 +22,6 @@ impl ToolHandler for ReportAgentJobResultHandler {
Some(create_report_agent_job_result_tool())
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
@@ -4,7 +4,6 @@ use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::agent_jobs_spec::create_spawn_agents_on_csv_tool;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
@@ -23,10 +22,6 @@ impl ToolHandler for SpawnAgentsOnCsvHandler {
Some(create_spawn_agents_on_csv_tool())
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
@@ -30,7 +30,6 @@ use crate::tools::registry::PostToolUsePayload;
use crate::tools::registry::PreToolUsePayload;
use crate::tools::registry::ToolArgumentDiffConsumer;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use crate::tools::runtimes::apply_patch::ApplyPatchRequest;
use crate::tools::runtimes::apply_patch::ApplyPatchRuntime;
use crate::tools::sandboxing::ToolCtx;
@@ -307,10 +306,6 @@ impl ToolHandler for ApplyPatchHandler {
Some(create_apply_patch_freeform_tool(self.multi_environment))
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Custom { .. })
}
@@ -6,7 +6,6 @@ use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use crate::turn_timing::now_unix_timestamp_ms;
use codex_protocol::dynamic_tools::DynamicToolCallRequest;
use codex_protocol::dynamic_tools::DynamicToolResponse;
@@ -36,10 +35,6 @@ impl ToolHandler for DynamicToolHandler {
self.tool_name.clone()
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
async fn is_mutating(&self, _invocation: &ToolInvocation) -> bool {
true
}
@@ -17,7 +17,6 @@ use crate::tools::hook_names::HookToolName;
use crate::tools::registry::PostToolUsePayload;
use crate::tools::registry::PreToolUsePayload;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
pub(crate) struct BundledToolOutput {
value: Value,
@@ -80,10 +79,6 @@ impl ToolHandler for BundledToolHandler {
Some(self.spec.clone())
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
self.arguments_from_payload(payload).is_some()
}
@@ -7,7 +7,6 @@ use crate::tools::handlers::goal_spec::CREATE_GOAL_TOOL_NAME;
use crate::tools::handlers::goal_spec::create_create_goal_tool;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
@@ -29,10 +28,6 @@ impl ToolHandler for CreateGoalHandler {
Some(create_create_goal_tool())
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session,
@@ -5,7 +5,6 @@ use crate::tools::context::ToolPayload;
use crate::tools::handlers::goal_spec::GET_GOAL_TOOL_NAME;
use crate::tools::handlers::goal_spec::create_get_goal_tool;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
@@ -26,10 +25,6 @@ impl ToolHandler for GetGoalHandler {
Some(create_get_goal_tool())
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session, payload, ..
@@ -8,7 +8,6 @@ use crate::tools::handlers::goal_spec::UPDATE_GOAL_TOOL_NAME;
use crate::tools::handlers::goal_spec::create_update_goal_tool;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_protocol::protocol::ThreadGoalStatus;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
@@ -31,10 +30,6 @@ impl ToolHandler for UpdateGoalHandler {
Some(create_update_goal_tool())
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session,
+59 -44
View File
@@ -8,22 +8,22 @@ use crate::tools::context::McpToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolPayload;
use crate::tools::flat_tool_name;
use crate::tools::hook_names::HookToolName;
use crate::tools::registry::PostToolUsePayload;
use crate::tools::registry::PreToolUsePayload;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use crate::tools::registry::ToolTelemetryTags;
use codex_mcp::ToolInfo;
use codex_tools::ToolName;
use serde_json::Value;
pub struct McpHandler {
tool_name: ToolName,
tool_info: ToolInfo,
}
impl McpHandler {
pub fn new(tool_name: ToolName) -> Self {
Self { tool_name }
pub fn new(tool_info: ToolInfo) -> Self {
Self { tool_info }
}
}
@@ -31,22 +31,29 @@ impl ToolHandler for McpHandler {
type Output = McpToolOutput;
fn tool_name(&self) -> ToolName {
self.tool_name.clone()
self.tool_info.canonical_tool_name()
}
fn kind(&self) -> ToolKind {
ToolKind::Mcp
fn supports_parallel_tool_calls(&self) -> bool {
self.tool_info.supports_parallel_tool_calls
}
async fn telemetry_tags(&self, _invocation: &ToolInvocation) -> ToolTelemetryTags {
let mut tags = vec![("mcp_server", self.tool_info.server_name.clone())];
if let Some(origin) = &self.tool_info.server_origin {
tags.push(("mcp_server_origin", origin.clone()));
}
tags
}
fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option<PreToolUsePayload> {
let ToolPayload::Mcp { raw_arguments, .. } = &invocation.payload else {
let ToolPayload::Function { arguments } = &invocation.payload else {
return None;
};
let tool_name = &self.tool_name;
Some(PreToolUsePayload {
tool_name: HookToolName::new(flat_tool_name(tool_name).into_owned()),
tool_input: mcp_hook_tool_input(raw_arguments),
tool_name: HookToolName::new(self.tool_name().to_string()),
tool_input: mcp_hook_tool_input(arguments),
})
}
@@ -55,15 +62,14 @@ impl ToolHandler for McpHandler {
invocation: &ToolInvocation,
result: &Self::Output,
) -> Option<PostToolUsePayload> {
let ToolPayload::Mcp { .. } = &invocation.payload else {
let ToolPayload::Function { .. } = &invocation.payload else {
return None;
};
let tool_response =
result.post_tool_use_response(&invocation.call_id, &invocation.payload)?;
let tool_name = &self.tool_name;
Some(PostToolUsePayload {
tool_name: HookToolName::new(flat_tool_name(tool_name).into_owned()),
tool_name: HookToolName::new(self.tool_name().to_string()),
tool_use_id: invocation.call_id.clone(),
tool_input: result.tool_input.clone(),
tool_response,
@@ -80,11 +86,7 @@ impl ToolHandler for McpHandler {
} = invocation;
let payload = match payload {
ToolPayload::Mcp {
server,
tool,
raw_arguments,
} => (server, tool, raw_arguments),
ToolPayload::Function { arguments } => arguments,
_ => {
return Err(FunctionCallError::RespondToModel(
"mcp handler received unsupported payload".to_string(),
@@ -92,19 +94,15 @@ impl ToolHandler for McpHandler {
}
};
let (server, tool, raw_arguments) = payload;
let arguments_str = raw_arguments;
let started = Instant::now();
let hook_tool_name = flat_tool_name(&self.tool_name);
let result = handle_mcp_tool_call(
Arc::clone(&session),
&turn,
call_id.clone(),
server,
tool,
hook_tool_name.into_owned(),
arguments_str,
self.tool_info.server_name.clone(),
self.tool_info.tool.name.to_string(),
self.tool_name().to_string(),
payload,
)
.await;
@@ -139,10 +137,8 @@ mod tests {
#[tokio::test]
async fn mcp_pre_tool_use_payload_uses_model_tool_name_and_raw_args() {
let payload = ToolPayload::Mcp {
server: "memory".to_string(),
tool: "create_entities".to_string(),
raw_arguments: json!({
let payload = ToolPayload::Function {
arguments: json!({
"entities": [{
"name": "Ada",
"entityType": "person"
@@ -151,10 +147,7 @@ mod tests {
.to_string(),
};
let (session, turn) = make_session_and_context().await;
let handler = McpHandler::new(codex_tools::ToolName::namespaced(
"mcp__memory__",
"create_entities",
));
let handler = McpHandler::new(tool_info("memory", "mcp__memory__", "create_entities"));
assert_eq!(
handler.pre_tool_use_payload(&ToolInvocation {
@@ -181,10 +174,8 @@ mod tests {
#[tokio::test]
async fn mcp_post_tool_use_payload_uses_model_tool_name_args_and_result() {
let payload = ToolPayload::Mcp {
server: "filesystem".to_string(),
tool: "read_file".to_string(),
raw_arguments: json!({ "path": "/tmp/notes.txt" }).to_string(),
let payload = ToolPayload::Function {
arguments: json!({ "path": "/tmp/notes.txt" }).to_string(),
};
let output = McpToolOutput {
result: codex_protocol::mcp::CallToolResult {
@@ -206,10 +197,7 @@ mod tests {
truncation_policy: codex_utils_output_truncation::TruncationPolicy::Bytes(1024),
};
let (session, turn) = make_session_and_context().await;
let handler = McpHandler::new(codex_tools::ToolName::namespaced(
"mcp__filesystem__",
"read_file",
));
let handler = McpHandler::new(tool_info("filesystem", "mcp__filesystem__", "read_file"));
let invocation = ToolInvocation {
session: session.into(),
turn: turn.into(),
@@ -245,4 +233,31 @@ mod tests {
fn mcp_hook_tool_input_defaults_empty_args_to_object() {
assert_eq!(mcp_hook_tool_input(" "), json!({}));
}
fn tool_info(server_name: &str, callable_namespace: &str, tool_name: &str) -> ToolInfo {
ToolInfo {
server_name: server_name.to_string(),
supports_parallel_tool_calls: false,
server_origin: None,
callable_name: tool_name.to_string(),
callable_namespace: callable_namespace.to_string(),
namespace_description: None,
tool: rmcp::model::Tool {
name: tool_name.to_string().into(),
title: None,
description: None,
input_schema: Arc::new(rmcp::model::object(serde_json::json!({
"type": "object",
}))),
output_schema: None,
annotations: None,
execution: None,
icons: None,
meta: None,
},
connector_id: None,
connector_name: None,
plugin_display_names: Vec::new(),
}
}
}
@@ -6,7 +6,6 @@ use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::mcp_resource_spec::create_list_mcp_resource_templates_tool;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_protocol::models::function_call_output_content_items_to_text;
use codex_protocol::protocol::McpInvocation;
use codex_tools::ToolName;
@@ -41,10 +40,6 @@ impl ToolHandler for ListMcpResourceTemplatesHandler {
true
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
#[expect(
clippy::await_holding_invalid_type,
reason = "MCP resource template listing reads through the session-owned manager guard"
@@ -6,7 +6,6 @@ use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::mcp_resource_spec::create_list_mcp_resources_tool;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_protocol::models::function_call_output_content_items_to_text;
use codex_protocol::protocol::McpInvocation;
use codex_tools::ToolName;
@@ -41,10 +40,6 @@ impl ToolHandler for ListMcpResourcesHandler {
true
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
#[expect(
clippy::await_holding_invalid_type,
reason = "MCP resource listing reads through the session-owned manager guard"
@@ -6,7 +6,6 @@ use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::mcp_resource_spec::create_read_mcp_resource_tool;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_protocol::models::function_call_output_content_items_to_text;
use codex_protocol::protocol::McpInvocation;
use codex_tools::ToolName;
@@ -41,10 +40,6 @@ impl ToolHandler for ReadMcpResourceHandler {
true
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session,
@@ -16,7 +16,6 @@ use crate::tools::context::ToolPayload;
pub(crate) use crate::tools::handlers::multi_agents_common::*;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_protocol::ThreadId;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::openai_models::ReasoningEffort;
@@ -16,10 +16,6 @@ impl ToolHandler for Handler {
Some(create_close_agent_tool_v1())
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
@@ -18,10 +18,6 @@ impl ToolHandler for Handler {
Some(create_resume_agent_tool())
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
@@ -17,10 +17,6 @@ impl ToolHandler for Handler {
Some(create_send_input_tool_v1())
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
@@ -33,10 +33,6 @@ impl ToolHandler for Handler {
Some(create_spawn_agent_tool_v1(self.options.clone()))
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
@@ -38,10 +38,6 @@ impl ToolHandler for Handler {
Some(create_wait_agent_tool_v1(self.options))
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
@@ -9,7 +9,6 @@ use crate::tools::context::ToolPayload;
use crate::tools::handlers::multi_agents_common::*;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_protocol::AgentPath;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::openai_models::ReasoningEffort;
@@ -16,10 +16,6 @@ impl ToolHandler for Handler {
Some(create_close_agent_tool_v2())
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
@@ -19,10 +19,6 @@ impl ToolHandler for Handler {
Some(create_followup_task_tool())
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
@@ -16,10 +16,6 @@ impl ToolHandler for Handler {
Some(create_list_agents_tool())
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
@@ -19,10 +19,6 @@ impl ToolHandler for Handler {
Some(create_send_message_tool())
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
@@ -35,10 +35,6 @@ impl ToolHandler for Handler {
Some(create_spawn_agent_tool_v2(self.options.clone()))
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
@@ -30,10 +30,6 @@ impl ToolHandler for Handler {
Some(create_wait_agent_tool_v2(self.options))
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
-5
View File
@@ -4,7 +4,6 @@ use crate::tools::context::ToolOutput;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::plan_spec::create_update_plan_tool;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_protocol::config_types::ModeKind;
use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::models::ResponseInputItem;
@@ -55,10 +54,6 @@ impl ToolHandler for PlanHandler {
Some(create_update_plan_tool())
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session,
@@ -9,7 +9,6 @@ use crate::tools::handlers::parse_arguments_with_base_path;
use crate::tools::handlers::shell_spec::create_request_permissions_tool;
use crate::tools::handlers::shell_spec::request_permissions_tool_description;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
@@ -28,10 +27,6 @@ impl ToolHandler for RequestPermissionsHandler {
))
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session,
@@ -35,7 +35,6 @@ use crate::tools::context::ToolPayload;
use crate::tools::handlers::parse_arguments;
use crate::tools::handlers::request_plugin_install_spec::create_request_plugin_install_tool;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
#[derive(Default)]
pub struct RequestPluginInstallHandler {
@@ -65,10 +64,6 @@ impl ToolHandler for RequestPluginInstallHandler {
true
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
#[expect(
clippy::await_holding_invalid_type,
reason = "plugin install discovery reads through the session-owned manager guard"
@@ -9,7 +9,6 @@ use crate::tools::handlers::request_user_input_spec::normalize_request_user_inpu
use crate::tools::handlers::request_user_input_spec::request_user_input_tool_description;
use crate::tools::handlers::request_user_input_spec::request_user_input_unavailable_message;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_protocol::config_types::ModeKind;
use codex_protocol::request_user_input::RequestUserInputArgs;
use codex_tools::ToolName;
@@ -32,10 +31,6 @@ impl ToolHandler for RequestUserInputHandler {
))
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session,
@@ -11,7 +11,6 @@ use crate::tools::handlers::resolve_workdir_base_path;
use crate::tools::registry::PostToolUsePayload;
use crate::tools::registry::PreToolUsePayload;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use crate::tools::runtimes::shell::ShellRuntimeBackend;
use super::RunExecLikeArgs;
@@ -29,10 +28,6 @@ impl ToolHandler for ContainerExecHandler {
ToolName::plain("container.exec")
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
@@ -10,7 +10,6 @@ use crate::tools::hook_names::HookToolName;
use crate::tools::registry::PostToolUsePayload;
use crate::tools::registry::PreToolUsePayload;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use crate::tools::runtimes::shell::ShellRuntimeBackend;
use codex_tools::ToolSpec;
@@ -46,10 +45,6 @@ impl ToolHandler for LocalShellHandler {
self.include_spec
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::LocalShell { .. })
}
@@ -21,7 +21,6 @@ use crate::tools::hook_names::HookToolName;
use crate::tools::registry::PostToolUsePayload;
use crate::tools::registry::PreToolUsePayload;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use crate::tools::runtimes::shell::ShellRuntimeBackend;
use codex_tools::ToolSpec;
@@ -144,10 +143,6 @@ impl ToolHandler for ShellCommandHandler {
self.options.is_some()
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
@@ -16,7 +16,6 @@ use crate::tools::handlers::resolve_workdir_base_path;
use crate::tools::registry::PostToolUsePayload;
use crate::tools::registry::PreToolUsePayload;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use crate::tools::runtimes::shell::ShellRuntimeBackend;
use codex_tools::ToolSpec;
@@ -78,10 +77,6 @@ impl ToolHandler for ShellHandler {
self.options.is_some()
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
@@ -15,7 +15,6 @@ use crate::tools::context::ToolPayload;
use crate::tools::handlers::parse_arguments;
use crate::tools::handlers::test_sync_spec::create_test_sync_tool;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
@@ -71,10 +70,6 @@ impl ToolHandler for TestSyncHandler {
true
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation { payload, .. } = invocation;
@@ -4,7 +4,6 @@ use crate::tools::context::ToolPayload;
use crate::tools::context::ToolSearchOutput;
use crate::tools::handlers::tool_search_spec::create_tool_search_tool;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use crate::tools::tool_search_entry::ToolSearchEntry;
use bm25::Document;
use bm25::Language;
@@ -68,10 +67,6 @@ impl ToolHandler for ToolSearchHandler {
true
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
async fn handle(
&self,
invocation: ToolInvocation,
@@ -399,6 +394,8 @@ mod tests {
fn tool_info(server_name: &str, tool_name: &str, description_prefix: &str) -> ToolInfo {
ToolInfo {
server_name: server_name.to_string(),
supports_parallel_tool_calls: false,
server_origin: None,
callable_name: tool_name.to_string(),
callable_namespace: format!("mcp__{server_name}__"),
namespace_description: None,
@@ -3,7 +3,6 @@ use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
@@ -48,10 +47,6 @@ impl ToolHandler for UnavailableToolHandler {
self.spec.clone()
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation { payload, .. } = invocation;
@@ -16,7 +16,6 @@ use crate::tools::hook_names::HookToolName;
use crate::tools::registry::PostToolUsePayload;
use crate::tools::registry::PreToolUsePayload;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use crate::unified_exec::ExecCommandRequest;
use crate::unified_exec::UnifiedExecContext;
use crate::unified_exec::UnifiedExecError;
@@ -88,10 +87,6 @@ impl ToolHandler for ExecCommandHandler {
true
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
@@ -5,7 +5,6 @@ use crate::tools::context::ToolPayload;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::PostToolUsePayload;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use crate::unified_exec::WriteStdinRequest;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::TerminalInteractionEvent;
@@ -42,10 +41,6 @@ impl ToolHandler for WriteStdinHandler {
Some(create_write_stdin_tool())
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
@@ -21,7 +21,6 @@ use crate::tools::handlers::resolve_tool_environment;
use crate::tools::handlers::view_image_spec::ViewImageToolOptions;
use crate::tools::handlers::view_image_spec::create_view_image_tool;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
@@ -77,10 +76,6 @@ impl ToolHandler for ViewImageHandler {
true
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
if !invocation
.turn
+53 -38
View File
@@ -32,11 +32,7 @@ use futures::future::BoxFuture;
use serde_json::Value;
use tracing::warn;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ToolKind {
Function,
Mcp,
}
pub(crate) type ToolTelemetryTags = Vec<(&'static str, String)>;
pub trait ToolHandler: Send + Sync {
type Output: ToolOutput + 'static;
@@ -52,17 +48,20 @@ pub trait ToolHandler: Send + Sync {
false
}
fn kind(&self) -> ToolKind;
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(
(self.kind(), payload),
(ToolKind::Function, ToolPayload::Function { .. })
| (ToolKind::Function, ToolPayload::ToolSearch { .. })
| (ToolKind::Mcp, ToolPayload::Mcp { .. })
payload,
ToolPayload::Function { .. } | ToolPayload::ToolSearch { .. }
)
}
fn telemetry_tags(
&self,
_invocation: &ToolInvocation,
) -> impl std::future::Future<Output = ToolTelemetryTags> + Send {
async { Vec::new() }
}
/// Returns `true` if the [ToolInvocation] *might* mutate the environment of the
/// user (through file system, OS operations, ...).
/// This function must remains defensive and return `true` if a doubt exist on the
@@ -168,12 +167,19 @@ pub(crate) struct PostToolUsePayload {
}
trait AnyToolHandler: Send + Sync {
fn supports_parallel_tool_calls(&self) -> bool;
fn matches_kind(&self, payload: &ToolPayload) -> bool;
fn is_mutating<'a>(&'a self, invocation: &'a ToolInvocation) -> BoxFuture<'a, bool>;
fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option<PreToolUsePayload>;
fn telemetry_tags<'a>(
&'a self,
invocation: &'a ToolInvocation,
) -> BoxFuture<'a, ToolTelemetryTags>;
fn create_diff_consumer(&self) -> Option<Box<dyn ToolArgumentDiffConsumer>>;
fn handle_any<'a>(
&'a self,
@@ -185,6 +191,10 @@ impl<T> AnyToolHandler for T
where
T: ToolHandler,
{
fn supports_parallel_tool_calls(&self) -> bool {
ToolHandler::supports_parallel_tool_calls(self)
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
ToolHandler::matches_kind(self, payload)
}
@@ -197,6 +207,13 @@ where
ToolHandler::pre_tool_use_payload(self, invocation)
}
fn telemetry_tags<'a>(
&'a self,
invocation: &'a ToolInvocation,
) -> BoxFuture<'a, ToolTelemetryTags> {
Box::pin(ToolHandler::telemetry_tags(self, invocation))
}
fn create_diff_consumer(&self) -> Option<Box<dyn ToolArgumentDiffConsumer>> {
ToolHandler::create_diff_consumer(self)
}
@@ -259,6 +276,10 @@ impl ToolRegistry {
self.handler(name)?.create_diff_consumer()
}
pub(crate) fn supports_parallel_tool_calls(&self, name: &ToolName) -> Option<bool> {
Some(self.handler(name)?.supports_parallel_tool_calls())
}
#[expect(
clippy::await_holding_invalid_type,
reason = "tool dispatch must keep active-turn accounting atomic"
@@ -273,7 +294,7 @@ impl ToolRegistry {
let otel = invocation.turn.session_telemetry.clone();
let payload_for_response = invocation.payload.clone();
let log_payload = payload_for_response.log_payload();
let metric_tags = [
let base_tool_result_tags = [
(
"sandbox",
permission_profile_sandbox_tag(
@@ -290,21 +311,6 @@ impl ToolRegistry {
),
),
];
let (mcp_server, mcp_server_origin) = match &invocation.payload {
ToolPayload::Mcp { server, .. } => {
let manager = invocation
.session
.services
.mcp_connection_manager
.read()
.await;
let origin = manager.server_origin(server).map(str::to_owned);
(Some(server.clone()), origin)
}
_ => (None, None),
};
let mcp_server_ref = mcp_server.as_deref();
let mcp_server_origin_ref = mcp_server_origin.as_deref();
{
let mut active = invocation.session.active_turn.lock().await;
@@ -315,7 +321,6 @@ impl ToolRegistry {
}
let dispatch_trace = ToolDispatchTrace::start(&invocation);
let handler = match self.handler(&tool_name) {
Some(handler) => handler,
None => {
@@ -327,9 +332,8 @@ impl ToolRegistry {
Duration::ZERO,
/*success*/ false,
&message,
&metric_tags,
mcp_server_ref,
mcp_server_origin_ref,
&base_tool_result_tags,
/*extra_trace_fields*/ &[],
);
let err = FunctionCallError::RespondToModel(message);
dispatch_trace.record_failed(&err);
@@ -337,6 +341,19 @@ impl ToolRegistry {
}
};
let telemetry_tags = handler.telemetry_tags(&invocation).await;
let mut tool_result_tags =
Vec::with_capacity(base_tool_result_tags.len() + telemetry_tags.len());
let mut extra_trace_fields = Vec::new();
tool_result_tags.extend_from_slice(&base_tool_result_tags);
for (key, value) in &telemetry_tags {
if matches!(*key, "mcp_server" | "mcp_server_origin") {
extra_trace_fields.push((*key, value.as_str()));
} else {
tool_result_tags.push((*key, value.as_str()));
}
}
if !handler.matches_kind(&invocation.payload) {
let message = format!("tool {tool_name} invoked with incompatible payload");
otel.tool_result_with_tags(
@@ -346,9 +363,8 @@ impl ToolRegistry {
Duration::ZERO,
/*success*/ false,
&message,
&metric_tags,
mcp_server_ref,
mcp_server_origin_ref,
&tool_result_tags,
&extra_trace_fields,
);
let err = FunctionCallError::Fatal(message);
dispatch_trace.record_failed(&err);
@@ -379,9 +395,8 @@ impl ToolRegistry {
tool_name_flat.as_ref(),
&call_id_owned,
log_payload.as_ref(),
&metric_tags,
mcp_server_ref,
mcp_server_origin_ref,
&tool_result_tags,
&extra_trace_fields,
|| {
let handler = handler.clone();
let response_cell = &response_cell;
@@ -406,7 +421,7 @@ impl ToolRegistry {
)
.await;
let success = match &result {
Ok((_preview, success)) => *success,
Ok((_, success)) => *success,
Err(_) => false,
};
emit_metric_for_tool_read(&invocation, success).await;
@@ -15,10 +15,6 @@ impl ToolHandler for TestHandler {
self.tool_name.clone()
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
async fn handle(&self, _invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
Ok(crate::tools::context::FunctionToolOutput::from_text(
"ok".to_string(),
+9 -32
View File
@@ -40,14 +40,12 @@ 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<Vec<ToolInfo>>,
pub(crate) deferred_mcp_tools: Option<Vec<ToolInfo>>,
pub(crate) unavailable_called_tools: Vec<ToolName>,
pub(crate) parallel_mcp_server_names: HashSet<String>,
pub(crate) discoverable_tools: Option<Vec<DiscoverableTool>>,
pub(crate) extension_tool_bundles: Vec<ExtensionToolBundle>,
pub(crate) dynamic_tools: &'a [DynamicToolSpec],
@@ -59,7 +57,6 @@ impl ToolRouter {
mcp_tools,
deferred_mcp_tools,
unavailable_called_tools,
parallel_mcp_server_names,
discoverable_tools,
extension_tool_bundles,
dynamic_tools,
@@ -99,7 +96,6 @@ impl ToolRouter {
registry,
specs,
model_visible_specs,
parallel_mcp_server_names,
}
}
@@ -166,20 +162,13 @@ impl ToolRouter {
}
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),
}
self.registry
.supports_parallel_tool_calls(&call.tool_name)
.unwrap_or_else(|| self.configured_tool_supports_parallel(&call.tool_name))
}
#[instrument(level = "trace", skip_all, err)]
pub async fn build_tool_call(
session: &Session,
item: ResponseItem,
) -> Result<Option<ToolCall>, FunctionCallError> {
pub fn build_tool_call(item: ResponseItem) -> Result<Option<ToolCall>, FunctionCallError> {
match item {
ResponseItem::FunctionCall {
name,
@@ -189,23 +178,11 @@ impl ToolRouter {
..
} => {
let tool_name = ToolName::new(namespace, name);
if let Some(tool_info) = session.resolve_mcp_tool_info(&tool_name).await {
Ok(Some(ToolCall {
tool_name: tool_info.canonical_tool_name(),
call_id,
payload: ToolPayload::Mcp {
server: tool_info.server_name,
tool: tool_info.tool.name.to_string(),
raw_arguments: arguments,
},
}))
} else {
Ok(Some(ToolCall {
tool_name,
call_id,
payload: ToolPayload::Function { arguments },
}))
}
Ok(Some(ToolCall {
tool_name,
call_id,
payload: ToolPayload::Function { arguments },
}))
}
ResponseItem::ToolSearchCall {
call_id: Some(call_id),
+64 -38
View File
@@ -1,4 +1,3 @@
use std::collections::HashSet;
use std::sync::Arc;
use crate::config::Config;
@@ -99,7 +98,6 @@ async fn parallel_support_does_not_match_namespaced_local_tool_names() -> anyhow
deferred_mcp_tools: None,
mcp_tools: Some(mcp_tools),
unavailable_called_tools: Vec::new(),
parallel_mcp_server_names: HashSet::new(),
discoverable_tools: None,
extension_tool_bundles: Vec::new(),
dynamic_tools: turn.dynamic_tools.as_slice(),
@@ -132,21 +130,15 @@ async fn parallel_support_does_not_match_namespaced_local_tool_names() -> anyhow
#[tokio::test]
async fn build_tool_call_uses_namespace_for_registry_name() -> anyhow::Result<()> {
let (session, _) = make_session_and_context().await;
let session = Arc::new(session);
let tool_name = "create_event".to_string();
let call = ToolRouter::build_tool_call(
&session,
ResponseItem::FunctionCall {
id: None,
name: tool_name.clone(),
namespace: Some("mcp__codex_apps__calendar".to_string()),
arguments: "{}".to_string(),
call_id: "call-namespace".to_string(),
},
)
.await?
let call = ToolRouter::build_tool_call(ResponseItem::FunctionCall {
id: None,
name: tool_name.clone(),
namespace: Some("mcp__codex_apps__calendar".to_string()),
arguments: "{}".to_string(),
call_id: "call-namespace".to_string(),
})?
.expect("function_call should produce a tool call");
assert_eq!(
@@ -171,9 +163,21 @@ async fn mcp_parallel_support_uses_exact_payload_server() -> anyhow::Result<()>
&turn.tools_config,
ToolRouterParams {
deferred_mcp_tools: None,
mcp_tools: None,
mcp_tools: Some(vec![
mcp_tool_info(
"echo",
/*supports_parallel_tool_calls*/ true,
"mcp__echo__",
"query_with_delay",
),
mcp_tool_info(
"hello_echo",
/*supports_parallel_tool_calls*/ false,
"mcp__hello_echo__",
"query_with_delay",
),
]),
unavailable_called_tools: Vec::new(),
parallel_mcp_server_names: HashSet::from(["echo".to_string()]),
discoverable_tools: None,
extension_tool_bundles: Vec::new(),
dynamic_tools: turn.dynamic_tools.as_slice(),
@@ -183,10 +187,8 @@ async fn mcp_parallel_support_uses_exact_payload_server() -> anyhow::Result<()>
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(),
payload: ToolPayload::Function {
arguments: "{}".to_string(),
},
};
assert!(router.tool_supports_parallel(&deferred_call));
@@ -194,10 +196,8 @@ async fn mcp_parallel_support_uses_exact_payload_server() -> anyhow::Result<()>
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(),
payload: ToolPayload::Function {
arguments: "{}".to_string(),
},
};
assert!(!router.tool_supports_parallel(&different_server_call));
@@ -241,7 +241,6 @@ async fn model_visible_specs_filter_deferred_dynamic_tools() -> anyhow::Result<(
deferred_mcp_tools: None,
mcp_tools: None,
unavailable_called_tools: Vec::new(),
parallel_mcp_server_names: HashSet::new(),
discoverable_tools: None,
extension_tool_bundles: Vec::new(),
dynamic_tools: &dynamic_tools,
@@ -265,6 +264,38 @@ async fn model_visible_specs_filter_deferred_dynamic_tools() -> anyhow::Result<(
Ok(())
}
fn mcp_tool_info(
server_name: &str,
supports_parallel_tool_calls: bool,
callable_namespace: &str,
tool_name: &str,
) -> codex_mcp::ToolInfo {
codex_mcp::ToolInfo {
server_name: server_name.to_string(),
supports_parallel_tool_calls,
server_origin: None,
callable_name: tool_name.to_string(),
callable_namespace: callable_namespace.to_string(),
namespace_description: None,
tool: rmcp::model::Tool {
name: tool_name.to_string().into(),
title: None,
description: Some("Test MCP tool".to_string().into()),
input_schema: Arc::new(rmcp::model::object(json!({
"type": "object",
}))),
output_schema: None,
annotations: None,
execution: None,
icons: None,
meta: None,
},
connector_id: None,
connector_name: None,
plugin_display_names: Vec::new(),
}
}
#[tokio::test]
async fn extension_tool_bundles_are_model_visible_and_dispatchable() -> anyhow::Result<()> {
let (mut session, turn) = make_session_and_context().await;
@@ -276,7 +307,6 @@ async fn extension_tool_bundles_are_model_visible_and_dispatchable() -> anyhow::
deferred_mcp_tools: None,
mcp_tools: None,
unavailable_called_tools: Vec::new(),
parallel_mcp_server_names: HashSet::new(),
discoverable_tools: None,
extension_tool_bundles: extension_tool_bundles(&session),
dynamic_tools: turn.dynamic_tools.as_slice(),
@@ -297,17 +327,13 @@ async fn extension_tool_bundles_are_model_visible_and_dispatchable() -> anyhow::
"expected extension-provided tool to be visible to the model"
);
let call = ToolRouter::build_tool_call(
&session,
ResponseItem::FunctionCall {
id: None,
name: "extension_echo".to_string(),
namespace: None,
arguments: json!({ "message": "hello" }).to_string(),
call_id: "call-extension".to_string(),
},
)
.await?
let call = ToolRouter::build_tool_call(ResponseItem::FunctionCall {
id: None,
name: "extension_echo".to_string(),
namespace: None,
arguments: json!({ "message": "hello" }).to_string(),
call_id: "call-extension".to_string(),
})?
.expect("function_call should produce a tool call");
let result = router
+7 -25
View File
@@ -8,8 +8,6 @@ use crate::tools::handlers::multi_agents_spec::WaitAgentTimeoutOptions;
use crate::tools::registry::ToolRegistryBuilder;
use crate::tools::spec_plan::build_tool_registry_builder;
use crate::tools::spec_plan_types::ToolNamespace;
use crate::tools::spec_plan_types::ToolRegistryBuildDeferredTool;
use crate::tools::spec_plan_types::ToolRegistryBuildMcpTool;
use crate::tools::spec_plan_types::ToolRegistryBuildParams;
use codex_mcp::ToolInfo;
use codex_protocol::dynamic_tools::DynamicToolSpec;
@@ -35,20 +33,14 @@ pub(crate) fn tool_user_shell_type(user_shell: &Shell) -> ToolUserShellType {
}
}
struct McpToolPlanInputs<'a> {
mcp_tools: Vec<ToolRegistryBuildMcpTool<'a>>,
struct McpToolPlanInputs {
mcp_tools: Vec<ToolInfo>,
tool_namespaces: HashMap<String, ToolNamespace>,
}
fn map_mcp_tools_for_plan(mcp_tools: &[ToolInfo]) -> McpToolPlanInputs<'_> {
fn map_mcp_tools_for_plan(mcp_tools: &[ToolInfo]) -> McpToolPlanInputs {
McpToolPlanInputs {
mcp_tools: mcp_tools
.iter()
.map(|tool| ToolRegistryBuildMcpTool {
name: tool.canonical_tool_name(),
tool: &tool.tool,
})
.collect(),
mcp_tools: mcp_tools.to_vec(),
tool_namespaces: mcp_tools
.iter()
.map(|tool| {
@@ -78,17 +70,7 @@ pub(crate) fn build_specs_with_discoverable_tools(
use crate::tools::tool_search_entry::build_tool_search_entries_for_config;
let mcp_tool_plan_inputs = mcp_tools.as_deref().map(map_mcp_tools_for_plan);
let deferred_mcp_tool_sources = deferred_mcp_tools.as_ref().map(|tools| {
tools
.iter()
.map(|tool| ToolRegistryBuildDeferredTool {
name: tool.canonical_tool_name(),
server_name: tool.server_name.as_str(),
connector_name: tool.connector_name.as_deref(),
description: tool.namespace_description.as_deref(),
})
.collect::<Vec<_>>()
});
let deferred_mcp_tool_sources = deferred_mcp_tools.as_deref();
let default_agent_type_description =
crate::agent::role::spawn_tool_spec::build(&std::collections::BTreeMap::new());
let min_wait_timeout_ms = if config.multi_agent_v2 {
@@ -108,7 +90,7 @@ pub(crate) fn build_specs_with_discoverable_tools(
.collect::<Vec<_>>();
let tool_search_entries = build_tool_search_entries_for_config(
config,
deferred_mcp_tools.as_deref(),
deferred_mcp_tool_sources,
&deferred_dynamic_tools,
);
let mut builder = build_tool_registry_builder(
@@ -117,7 +99,7 @@ pub(crate) fn build_specs_with_discoverable_tools(
mcp_tools: mcp_tool_plan_inputs
.as_ref()
.map(|inputs| inputs.mcp_tools.as_slice()),
deferred_mcp_tools: deferred_mcp_tool_sources.as_deref(),
deferred_mcp_tools: deferred_mcp_tool_sources,
tool_namespaces: mcp_tool_plan_inputs
.as_ref()
.map(|inputs| &inputs.tool_namespaces),
+19 -17
View File
@@ -47,6 +47,7 @@ use crate::tools::hosted_spec::create_web_search_tool;
use crate::tools::registry::ToolRegistryBuilder;
use crate::tools::spec_plan_types::ToolRegistryBuildParams;
use crate::tools::spec_plan_types::agent_type_description;
use codex_mcp::ToolInfo;
use codex_protocol::openai_models::ConfigShellToolType;
use codex_tools::ResponsesApiNamespace;
use codex_tools::ResponsesApiNamespaceTool;
@@ -227,9 +228,9 @@ pub fn build_tool_registry_builder(
.map(|deferred_mcp_tools| {
collect_tool_search_source_infos(deferred_mcp_tools.iter().map(|tool| {
ToolSearchSource {
server_name: tool.server_name,
connector_name: tool.connector_name,
description: tool.description,
server_name: tool.server_name.as_str(),
connector_name: tool.connector_name.as_deref(),
description: tool.namespace_description.as_deref(),
}
}))
})
@@ -340,24 +341,26 @@ pub fn build_tool_registry_builder(
}
if let Some(mcp_tools) = params.mcp_tools {
let mut entries = mcp_tools.to_vec();
entries.sort_by(|a, b| a.name.cmp(&b.name));
let mut entries = mcp_tools
.iter()
.map(|tool| (tool.canonical_tool_name(), tool))
.collect::<Vec<_>>();
entries.sort_by(|(left, _), (right, _)| left.cmp(right));
let mut namespace_entries = BTreeMap::new();
for tool in entries {
let Some(namespace) = tool.name.namespace.as_ref() else {
let tool_name = &tool.name;
for (tool_name, tool) in entries {
let Some(namespace) = tool_name.namespace.as_ref() else {
tracing::error!("Skipping MCP tool `{tool_name}`: MCP tools must be namespaced");
continue;
};
namespace_entries
.entry(namespace.clone())
.or_insert_with(Vec::new)
.push(tool);
.push((tool_name, tool));
}
for (namespace, mut entries) in namespace_entries {
entries.sort_by_key(|tool| tool.name.name.clone());
entries.sort_by_key(|(tool_name, _)| tool_name.name.clone());
let tool_namespace = params
.tool_namespaces
.and_then(|namespaces| namespaces.get(&namespace));
@@ -373,14 +376,13 @@ pub fn build_tool_registry_builder(
default_namespace_description(namespace_name)
});
let mut tools = Vec::new();
for tool in entries {
match mcp_tool_to_responses_api_tool(&tool.name, tool.tool) {
for (tool_name, tool) in entries {
match mcp_tool_to_responses_api_tool(&tool_name, &tool.tool) {
Ok(converted_tool) => {
tools.push(ResponsesApiNamespaceTool::Function(converted_tool));
builder.register_handler(Arc::new(McpHandler::new(tool.name)));
builder.register_handler(Arc::new(McpHandler::new(tool.clone())));
}
Err(error) => {
let tool_name = &tool.name;
tracing::error!(
"Failed to convert `{tool_name}` MCP tool to OpenAI tool: {error:?}"
);
@@ -429,11 +431,11 @@ pub fn build_tool_registry_builder(
.mcp_tools
.into_iter()
.flatten()
.map(|direct| direct.name.clone())
.map(ToolInfo::canonical_tool_name)
.collect::<HashSet<_>>();
for tool in deferred_mcp_tools {
if !directly_registered_mcp_tools.contains(&tool.name) {
builder.register_handler(Arc::new(McpHandler::new(tool.name.clone())));
if !directly_registered_mcp_tools.contains(&tool.canonical_tool_name()) {
builder.register_handler(Arc::new(McpHandler::new(tool.clone())));
}
}
}
+57 -24
View File
@@ -26,11 +26,10 @@ use crate::tools::handlers::view_image_spec::ViewImageToolOptions;
use crate::tools::handlers::view_image_spec::create_view_image_tool;
use crate::tools::registry::ToolRegistry;
use crate::tools::spec_plan_types::ToolNamespace;
use crate::tools::spec_plan_types::ToolRegistryBuildDeferredTool;
use crate::tools::spec_plan_types::ToolRegistryBuildMcpTool;
use codex_app_server_protocol::AppInfo;
use codex_features::Feature;
use codex_features::Features;
use codex_mcp::ToolInfo;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::WebSearchConfig;
use codex_protocol::config_types::WebSearchMode;
@@ -2413,10 +2412,10 @@ fn search_capable_model_info() -> ModelInfo {
}
}
fn build_specs<'a>(
fn build_specs(
config: &ToolsConfig,
mcp_tools: Option<HashMap<ToolName, rmcp::model::Tool>>,
deferred_mcp_tools: Option<Vec<ToolRegistryBuildDeferredTool<'a>>>,
deferred_mcp_tools: Option<Vec<ToolInfo>>,
dynamic_tools: &[DynamicToolSpec],
) -> (Vec<ConfiguredToolSpec>, ToolRegistry) {
build_specs_with_discoverable_tools(
@@ -2429,10 +2428,10 @@ fn build_specs<'a>(
)
}
fn build_specs_with_discoverable_tools<'a>(
fn build_specs_with_discoverable_tools(
config: &ToolsConfig,
mcp_tools: Option<HashMap<ToolName, rmcp::model::Tool>>,
deferred_mcp_tools: Option<Vec<ToolRegistryBuildDeferredTool<'a>>>,
deferred_mcp_tools: Option<Vec<ToolInfo>>,
discoverable_tools: Option<Vec<DiscoverableTool>>,
extension_tool_bundles: &[codex_tool_api::ToolBundle],
dynamic_tools: &[DynamicToolSpec],
@@ -2448,10 +2447,10 @@ fn build_specs_with_discoverable_tools<'a>(
)
}
fn build_specs_with_optional_tool_namespaces<'a>(
fn build_specs_with_optional_tool_namespaces(
config: &ToolsConfig,
mcp_tools: Option<HashMap<ToolName, rmcp::model::Tool>>,
deferred_mcp_tools: Option<Vec<ToolRegistryBuildDeferredTool<'a>>>,
deferred_mcp_tools: Option<Vec<ToolInfo>>,
tool_namespaces: Option<HashMap<String, ToolNamespace>>,
discoverable_tools: Option<Vec<DiscoverableTool>>,
extension_tool_bundles: &[codex_tool_api::ToolBundle],
@@ -2460,10 +2459,7 @@ fn build_specs_with_optional_tool_namespaces<'a>(
let mcp_tool_inputs = mcp_tools.as_ref().map(|mcp_tools| {
mcp_tools
.iter()
.map(|(name, tool)| ToolRegistryBuildMcpTool {
name: name.clone(),
tool,
})
.map(|(name, tool)| tool_info_from_parts(name, tool.clone()))
.collect::<Vec<_>>()
});
let builder = build_tool_registry_builder(
@@ -2497,6 +2493,33 @@ fn mcp_tool(name: &str, description: &str, input_schema: serde_json::Value) -> r
}
}
fn tool_info_from_parts(name: &ToolName, tool: rmcp::model::Tool) -> ToolInfo {
ToolInfo {
server_name: server_name_from_tool_name(name),
supports_parallel_tool_calls: false,
server_origin: None,
callable_name: name.name.clone(),
callable_namespace: name.namespace.clone().unwrap_or_default(),
namespace_description: None,
tool,
connector_id: None,
connector_name: None,
plugin_display_names: Vec::new(),
}
}
fn server_name_from_tool_name(name: &ToolName) -> String {
name.namespace
.as_deref()
.and_then(|namespace| {
namespace
.strip_prefix("mcp__")
.and_then(|suffix| suffix.strip_suffix("__"))
})
.unwrap_or_else(|| name.namespace.as_deref().unwrap_or("test_server"))
.to_string()
}
#[test]
fn code_mode_augments_mcp_tool_descriptions_with_structured_output_sample() {
let model_info = model_info();
@@ -2588,18 +2611,28 @@ fn discoverable_connector(id: &str, name: &str, description: &str) -> Discoverab
}))
}
fn deferred_mcp_tool<'a>(
tool_name: &'a str,
tool_namespace: &'a str,
server_name: &'a str,
connector_name: Option<&'a str>,
description: Option<&'a str>,
) -> ToolRegistryBuildDeferredTool<'a> {
ToolRegistryBuildDeferredTool {
name: ToolName::namespaced(tool_namespace, tool_name),
server_name,
connector_name,
description,
fn deferred_mcp_tool(
tool_name: &str,
tool_namespace: &str,
server_name: &str,
connector_name: Option<&str>,
description: Option<&str>,
) -> ToolInfo {
ToolInfo {
server_name: server_name.to_string(),
supports_parallel_tool_calls: false,
server_origin: None,
callable_name: tool_name.to_string(),
callable_namespace: tool_namespace.to_string(),
namespace_description: description.map(str::to_string),
tool: mcp_tool(
tool_name,
description.unwrap_or("Deferred MCP tool"),
json!({}),
),
connector_id: None,
connector_name: connector_name.map(str::to_string),
plugin_display_names: Vec::new(),
}
}
+3 -20
View File
@@ -1,15 +1,15 @@
use crate::tools::handlers::multi_agents_spec::WaitAgentTimeoutOptions;
use codex_mcp::ToolInfo;
use codex_protocol::dynamic_tools::DynamicToolSpec;
use codex_tool_api::ToolBundle as ExtensionToolBundle;
use codex_tools::DiscoverableTool;
use codex_tools::ToolName;
use codex_tools::ToolsConfig;
use std::collections::HashMap;
#[derive(Clone, Copy)]
pub struct ToolRegistryBuildParams<'a> {
pub mcp_tools: Option<&'a [ToolRegistryBuildMcpTool<'a>]>,
pub deferred_mcp_tools: Option<&'a [ToolRegistryBuildDeferredTool<'a>]>,
pub mcp_tools: Option<&'a [ToolInfo]>,
pub deferred_mcp_tools: Option<&'a [ToolInfo]>,
pub tool_namespaces: Option<&'a HashMap<String, ToolNamespace>>,
pub discoverable_tools: Option<&'a [DiscoverableTool]>,
pub extension_tool_bundles: &'a [ExtensionToolBundle],
@@ -25,23 +25,6 @@ pub struct ToolNamespace {
pub description: Option<String>,
}
/// Direct MCP tool metadata needed to expose the Responses API namespace tool
/// while registering its runtime handler with the canonical namespace/name
/// identity.
#[derive(Debug, Clone)]
pub struct ToolRegistryBuildMcpTool<'a> {
pub name: ToolName,
pub tool: &'a rmcp::model::Tool,
}
#[derive(Debug, Clone)]
pub struct ToolRegistryBuildDeferredTool<'a> {
pub name: ToolName,
pub server_name: &'a str,
pub connector_name: Option<&'a str>,
pub description: Option<&'a str>,
}
pub(crate) fn agent_type_description(
config: &ToolsConfig,
default_agent_type_description: &str,
+12 -1
View File
@@ -60,6 +60,8 @@ fn mcp_tool(name: &str, description: &str, input_schema: serde_json::Value) -> r
fn mcp_tool_info(tool: rmcp::model::Tool) -> ToolInfo {
ToolInfo {
server_name: "test_server".to_string(),
supports_parallel_tool_calls: false,
server_origin: None,
callable_name: tool.name.to_string(),
callable_namespace: "mcp__test_server__".to_string(),
namespace_description: None,
@@ -78,6 +80,8 @@ fn mcp_tool_info_with_display_name(display_name: &str, tool: rmcp::model::Tool)
ToolInfo {
server_name: "test_server".to_string(),
supports_parallel_tool_calls: false,
server_origin: None,
callable_name,
callable_namespace,
namespace_description: None,
@@ -352,7 +356,6 @@ async fn assert_model_tools(
mcp_tools: None,
deferred_mcp_tools: None,
unavailable_called_tools: Vec::new(),
parallel_mcp_server_names: std::collections::HashSet::new(),
discoverable_tools: None,
extension_tool_bundles: Vec::new(),
dynamic_tools: &[],
@@ -895,6 +898,8 @@ async fn search_tool_description_falls_back_to_connector_name_without_descriptio
/*mcp_tools*/ None,
Some(vec![ToolInfo {
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
supports_parallel_tool_calls: false,
server_origin: None,
callable_name: "_create_event".to_string(),
callable_namespace: "mcp__codex_apps__calendar".to_string(),
namespace_description: None,
@@ -943,6 +948,8 @@ async fn search_tool_registers_namespaced_mcp_tool_aliases() {
Some(vec![
ToolInfo {
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
supports_parallel_tool_calls: false,
server_origin: None,
callable_name: "_create_event".to_string(),
callable_namespace: "mcp__codex_apps__calendar".to_string(),
namespace_description: None,
@@ -957,6 +964,8 @@ async fn search_tool_registers_namespaced_mcp_tool_aliases() {
},
ToolInfo {
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
supports_parallel_tool_calls: false,
server_origin: None,
callable_name: "_list_events".to_string(),
callable_namespace: "mcp__codex_apps__calendar".to_string(),
namespace_description: None,
@@ -971,6 +980,8 @@ async fn search_tool_registers_namespaced_mcp_tool_aliases() {
},
ToolInfo {
server_name: "rmcp".to_string(),
supports_parallel_tool_calls: false,
server_origin: None,
callable_name: "echo".to_string(),
callable_namespace: "mcp__rmcp__".to_string(),
namespace_description: None,
@@ -120,15 +120,6 @@ fn tool_dispatch_payload(payload: &ToolPayload) -> ToolDispatchPayload {
additional_permissions: params.additional_permissions.clone(),
justification: params.justification.clone(),
},
ToolPayload::Mcp {
server,
tool,
raw_arguments,
} => ToolDispatchPayload::Mcp {
server: server.clone(),
tool: tool.clone(),
raw_arguments: raw_arguments.clone(),
},
}
}
@@ -22,7 +22,6 @@ use crate::tools::context::ToolCallSource;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use crate::tools::registry::ToolRegistry;
use crate::turn_diff_tracker::TurnDiffTracker;
@@ -37,10 +36,6 @@ impl ToolHandler for TestHandler {
self.tool_name.clone()
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
async fn handle(&self, _invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
Ok(FunctionToolOutput::from_text("ok".to_string(), Some(true)))
}
+12 -8
View File
@@ -65,6 +65,12 @@ const RESPONSES_API_ENGINE_SERVICE_TTFT_FIELD: &str = "engine_service_ttft_total
const RESPONSES_API_ENGINE_IAPI_TBT_FIELD: &str = "engine_iapi_tbt_across_engine_calls_ms";
const RESPONSES_API_ENGINE_SERVICE_TBT_FIELD: &str = "engine_service_tbt_across_engine_calls_ms";
fn trace_field_value<'a>(fields: &'a [(&str, &str)], key: &str) -> Option<&'a str> {
fields
.iter()
.find_map(|(field_key, value)| (*field_key == key).then_some(*value))
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct AuthEnvTelemetryMetadata {
pub openai_api_key_env_present: bool,
@@ -943,8 +949,7 @@ impl SessionTelemetry {
call_id: &str,
arguments: &str,
extra_tags: &[(&str, &str)],
mcp_server: Option<&str>,
mcp_server_origin: Option<&str>,
extra_trace_fields: &[(&str, &str)],
f: F,
) -> Result<(String, bool), E>
where
@@ -969,8 +974,7 @@ impl SessionTelemetry {
success,
output.as_ref(),
extra_tags,
mcp_server,
mcp_server_origin,
extra_trace_fields,
);
result
@@ -1010,8 +1014,7 @@ impl SessionTelemetry {
success: bool,
output: &str,
extra_tags: &[(&str, &str)],
mcp_server: Option<&str>,
mcp_server_origin: Option<&str>,
extra_trace_fields: &[(&str, &str)],
) {
let success_str = if success { "true" } else { "false" };
let mut tags = Vec::with_capacity(2 + extra_tags.len());
@@ -1020,8 +1023,9 @@ impl SessionTelemetry {
tags.extend_from_slice(extra_tags);
self.counter(TOOL_CALL_COUNT_METRIC, /*inc*/ 1, &tags);
self.record_duration(TOOL_CALL_DURATION_METRIC, duration, &tags);
let mcp_server = mcp_server.unwrap_or("");
let mcp_server_origin = mcp_server_origin.unwrap_or("");
let mcp_server = trace_field_value(extra_trace_fields, "mcp_server").unwrap_or("");
let mcp_server_origin =
trace_field_value(extra_trace_fields, "mcp_server_origin").unwrap_or("");
log_event!(
self,
event.name = "codex.tool_result",
@@ -249,8 +249,10 @@ fn otel_export_routing_policy_routes_tool_result_log_and_trace_events() {
/*success*/ true,
"secret output\nsecond line",
&[],
Some("internal-mcp"),
Some("stdio"),
&[
("mcp_server", "internal-mcp"),
("mcp_server_origin", "stdio"),
],
);
});
@@ -277,6 +279,10 @@ fn otel_export_routing_policy_routes_tool_result_log_and_trace_events() {
tool_log_attrs.get("mcp_server").map(String::as_str),
Some("internal-mcp")
);
assert_eq!(
tool_log_attrs.get("mcp_server_origin").map(String::as_str),
Some("stdio")
);
let spans = span_exporter.get_finished_spans().expect("span export");
assert_eq!(spans.len(), 1);
@@ -299,14 +305,6 @@ fn otel_export_routing_policy_routes_tool_result_log_and_trace_events() {
.map(String::as_str),
Some("2")
);
assert_eq!(
tool_trace_attrs.get("tool_origin").map(String::as_str),
Some("mcp")
);
assert_eq!(
tool_trace_attrs.get("mcp_tool").map(String::as_str),
Some("true")
);
assert!(!tool_trace_attrs.contains_key("arguments"));
assert!(!tool_trace_attrs.contains_key("output"));
assert!(!tool_trace_attrs.contains_key("mcp_server"));
+1 -2
View File
@@ -44,8 +44,7 @@ fn runtime_metrics_summary_collects_tool_api_and_streaming_metrics() -> Result<(
/*success*/ true,
"ok",
&[],
/*mcp_server*/ None,
/*mcp_server_origin*/ None,
/*extra_trace_fields*/ &[],
);
manager.record_api_request(
/*attempt*/ 1,
+2 -29
View File
@@ -93,11 +93,6 @@ pub enum ToolDispatchPayload {
additional_permissions: Option<AdditionalPermissionProfile>,
justification: Option<String>,
},
Mcp {
server: String,
tool: String,
raw_arguments: String,
},
}
/// Result data returned from a dispatch-level tool call.
@@ -263,14 +258,7 @@ fn requester_fields(
}
}
fn dispatched_tool_kind(tool_name: &str, payload: &ToolDispatchPayload) -> ToolCallKind {
if let ToolDispatchPayload::Mcp { server, tool, .. } = payload {
return ToolCallKind::Mcp {
server: server.clone(),
tool: tool.clone(),
};
}
fn dispatched_tool_kind(tool_name: &str, _payload: &ToolDispatchPayload) -> ToolCallKind {
match tool_name {
"exec_command" | "local_shell" | "shell" | "shell_command" => ToolCallKind::ExecCommand,
"write_stdin" => ToolCallKind::WriteStdin,
@@ -291,12 +279,8 @@ fn dispatched_tool_kind(tool_name: &str, payload: &ToolDispatchPayload) -> ToolC
fn dispatched_tool_label(
tool_name: &str,
tool_namespace: Option<&str>,
payload: &ToolDispatchPayload,
_payload: &ToolDispatchPayload,
) -> String {
if let ToolDispatchPayload::Mcp { server, tool, .. } = payload {
return format!("mcp:{server}:{tool}");
}
match tool_namespace {
Some(namespace) => format!("{namespace}.{tool_name}"),
None => tool_name.to_string(),
@@ -310,7 +294,6 @@ impl ToolDispatchPayload {
ToolDispatchPayload::ToolSearch { arguments } => truncate_preview(&arguments.query),
ToolDispatchPayload::Custom { input } => truncate_preview(input),
ToolDispatchPayload::LocalShell { command, .. } => truncate_preview(&command.join(" ")),
ToolDispatchPayload::Mcp { raw_arguments, .. } => truncate_preview(raw_arguments),
}
}
@@ -346,16 +329,6 @@ impl ToolDispatchPayload {
"additional_permissions": additional_permissions,
"justification": justification,
}),
ToolDispatchPayload::Mcp {
server,
tool,
raw_arguments,
} => json!({
"type": "mcp",
"server": server,
"tool": tool,
"raw_arguments": raw_arguments,
}),
}
}
}