mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Move MCP tool naming mode into manager (#21576)
## Why The `non_prefixed_mcp_tool_names` feature should be applied where MCP tools become model-visible, not by remapping names later in core. Keeping the decision in `McpConnectionManager` construction makes `ToolInfo` the single shaped view that spec building, deferred tool search, routing, and unavailable-tool placeholders can consume directly. This also preserves the existing external behavior while the feature is off, and keeps the feature-on behavior for code mode and hooks explicit at the manager boundary. ## What Changed - Add `McpToolNameMode` to `codex-mcp` and flow it through `McpConfig` into `McpConnectionManager::new`. - Normalize MCP `ToolInfo` names in the manager using either legacy-prefixed namespaces or non-prefixed namespaces; the legacy path adds `mcp__` without restoring the old trailing namespace suffix. - Remove the core-side MCP name remapping path so specs, tool search, session resolution, and unavailable-tool placeholder construction use the manager-provided `ToolName` values directly. - Keep code mode flattening on the `__` namespace separator. - Preserve hook compatibility by giving non-prefixed MCP hook names legacy `mcp__...` matcher aliases. - Add/adjust integration and unit coverage for non-prefixed code-mode behavior, hook matching with the feature on and off, and manager-level legacy prefixing. ## Testing - `cargo test -p codex-mcp --lib` - `cargo test -p codex-core --lib tools::spec::tests -- --nocapture` - `cargo test -p codex-core --lib mcp_tools -- --nocapture` - `cargo test -p codex-core --lib mcp_tool_exposure -- --nocapture` - `cargo test -p codex-core --test all mcp_tool -- --nocapture` - `cargo test -p codex-core --test all search_tool -- --nocapture` - `cargo test -p codex-core --test all hooks_mcp -- --nocapture` - `cargo test -p codex-core --test all code_mode_uses_non_prefixed_mcp_tool_names_when_feature_enabled -- --nocapture` - `cargo test -p codex-tools` - `cargo test -p codex-features`
This commit is contained in:
@@ -9,7 +9,10 @@ use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::context::boxed_tool_output;
|
||||
use crate::tools::flat_tool_name;
|
||||
use crate::tools::hook_names::HookToolName;
|
||||
use crate::tools::registry::CoreToolRuntime;
|
||||
use crate::tools::registry::PostToolUsePayload;
|
||||
use crate::tools::registry::PreToolUsePayload;
|
||||
use crate::tools::registry::ToolExecutor;
|
||||
use crate::tools::registry::ToolTelemetryTags;
|
||||
use crate::tools::tool_search_entry::ToolSearchInfo;
|
||||
@@ -20,6 +23,11 @@ use codex_tools::ToolName;
|
||||
use codex_tools::ToolSearchSourceInfo;
|
||||
use codex_tools::ToolSpec;
|
||||
use codex_tools::mcp_tool_to_responses_api_tool;
|
||||
use serde_json::Map;
|
||||
use serde_json::Value;
|
||||
|
||||
const LEGACY_MCP_TOOL_NAME_PREFIX: &str = "mcp__";
|
||||
const MCP_TOOL_NAME_DELIMITER: &str = "__";
|
||||
|
||||
pub struct McpHandler {
|
||||
tool_info: ToolInfo,
|
||||
@@ -31,6 +39,29 @@ impl McpHandler {
|
||||
let spec = create_tool_spec(&tool_info)?;
|
||||
Ok(Self { tool_info, spec })
|
||||
}
|
||||
|
||||
fn hook_tool_name(&self) -> HookToolName {
|
||||
HookToolName::new(ensure_mcp_prefix(&join_tool_name(&self.tool_name())))
|
||||
}
|
||||
}
|
||||
|
||||
fn join_tool_name(tool_name: &ToolName) -> String {
|
||||
match tool_name.namespace.as_deref() {
|
||||
Some(namespace) => {
|
||||
let namespace = namespace.trim_end_matches('_');
|
||||
let name = tool_name.name.trim_start_matches('_');
|
||||
format!("{namespace}{MCP_TOOL_NAME_DELIMITER}{name}")
|
||||
}
|
||||
None => tool_name.name.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_mcp_prefix(name: &str) -> String {
|
||||
if name.starts_with(LEGACY_MCP_TOOL_NAME_PREFIX) {
|
||||
name.to_string()
|
||||
} else {
|
||||
format!("{LEGACY_MCP_TOOL_NAME_PREFIX}{name}")
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
@@ -84,7 +115,7 @@ impl ToolExecutor<ToolInvocation> for McpHandler {
|
||||
call_id.clone(),
|
||||
self.tool_info.server_name.clone(),
|
||||
self.tool_info.tool.name.to_string(),
|
||||
self.tool_name().to_string(),
|
||||
self.hook_tool_name(),
|
||||
payload,
|
||||
)
|
||||
.await;
|
||||
@@ -138,6 +169,58 @@ impl CoreToolRuntime for McpHandler {
|
||||
tags
|
||||
})
|
||||
}
|
||||
|
||||
fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option<PreToolUsePayload> {
|
||||
let ToolPayload::Function { arguments } = &invocation.payload else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(PreToolUsePayload {
|
||||
tool_name: self.hook_tool_name(),
|
||||
tool_input: mcp_hook_tool_input(arguments),
|
||||
})
|
||||
}
|
||||
|
||||
fn with_updated_hook_input(
|
||||
&self,
|
||||
mut invocation: ToolInvocation,
|
||||
updated_input: Value,
|
||||
) -> Result<ToolInvocation, FunctionCallError> {
|
||||
invocation.payload = match invocation.payload {
|
||||
ToolPayload::Function { .. } => ToolPayload::Function {
|
||||
arguments: serde_json::to_string(&updated_input).map_err(|err| {
|
||||
FunctionCallError::RespondToModel(format!(
|
||||
"failed to serialize rewritten MCP arguments: {err}"
|
||||
))
|
||||
})?,
|
||||
},
|
||||
payload => {
|
||||
return Err(FunctionCallError::RespondToModel(format!(
|
||||
"tool {} does not support hook input rewriting for payload {payload:?}",
|
||||
self.tool_name()
|
||||
)));
|
||||
}
|
||||
};
|
||||
Ok(invocation)
|
||||
}
|
||||
fn post_tool_use_payload(
|
||||
&self,
|
||||
invocation: &ToolInvocation,
|
||||
result: &dyn crate::tools::context::ToolOutput,
|
||||
) -> Option<PostToolUsePayload> {
|
||||
let ToolPayload::Function { .. } = &invocation.payload else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let tool_response =
|
||||
result.post_tool_use_response(&invocation.call_id, &invocation.payload)?;
|
||||
Some(PostToolUsePayload {
|
||||
tool_name: self.hook_tool_name(),
|
||||
tool_use_id: invocation.call_id.clone(),
|
||||
tool_input: result.post_tool_use_input(&invocation.payload)?,
|
||||
tool_response,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn create_tool_spec(tool_info: &ToolInfo) -> Result<ToolSpec, serde_json::Error> {
|
||||
@@ -166,6 +249,14 @@ fn create_tool_spec(tool_info: &ToolInfo) -> Result<ToolSpec, serde_json::Error>
|
||||
}))
|
||||
}
|
||||
|
||||
fn mcp_hook_tool_input(raw_arguments: &str) -> Value {
|
||||
if raw_arguments.trim().is_empty() {
|
||||
return Value::Object(Map::new());
|
||||
}
|
||||
|
||||
serde_json::from_str(raw_arguments).unwrap_or_else(|_| Value::String(raw_arguments.to_string()))
|
||||
}
|
||||
|
||||
fn build_mcp_search_text(info: &ToolInfo) -> String {
|
||||
let tool_name = info.canonical_tool_name();
|
||||
let mut schema_properties = info
|
||||
@@ -233,7 +324,7 @@ mod tests {
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[tokio::test]
|
||||
async fn mcp_pre_tool_use_payload_uses_model_tool_name_and_raw_args() {
|
||||
async fn mcp_pre_tool_use_payload_uses_prefixed_tool_name_and_raw_args() {
|
||||
let payload = ToolPayload::Function {
|
||||
arguments: json!({
|
||||
"entities": [{
|
||||
@@ -244,7 +335,7 @@ mod tests {
|
||||
.to_string(),
|
||||
};
|
||||
let (session, turn) = make_session_and_context().await;
|
||||
let handler = McpHandler::new(tool_info("memory", "mcp__memory__", "create_entities"))
|
||||
let handler = McpHandler::new(tool_info("memory", "memory", "create_entities"))
|
||||
.expect("MCP tool spec should build");
|
||||
assert_eq!(
|
||||
handler.pre_tool_use_payload(&ToolInvocation {
|
||||
@@ -253,7 +344,7 @@ mod tests {
|
||||
cancellation_token: tokio_util::sync::CancellationToken::new(),
|
||||
tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
|
||||
call_id: "call-mcp-pre".to_string(),
|
||||
tool_name: codex_tools::ToolName::namespaced("mcp__memory__", "create_entities"),
|
||||
tool_name: codex_tools::ToolName::namespaced("memory", "create_entities"),
|
||||
source: ToolCallSource::Direct,
|
||||
payload,
|
||||
}),
|
||||
@@ -275,7 +366,7 @@ mod tests {
|
||||
arguments: json!({ "message": "hello" }).to_string(),
|
||||
};
|
||||
let (session, turn) = make_session_and_context().await;
|
||||
let handler = McpHandler::new(tool_info("foo", "mcp__foo__", "exec_command"))
|
||||
let handler = McpHandler::new(tool_info("foo", "mcp__foo", "exec_command"))
|
||||
.expect("MCP tool spec should build");
|
||||
|
||||
assert_eq!(
|
||||
@@ -285,7 +376,7 @@ mod tests {
|
||||
cancellation_token: tokio_util::sync::CancellationToken::new(),
|
||||
tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
|
||||
call_id: "call-mcp-pre-builtin-like".to_string(),
|
||||
tool_name: codex_tools::ToolName::namespaced("mcp__foo__", "exec_command"),
|
||||
tool_name: codex_tools::ToolName::namespaced("mcp__foo", "exec_command"),
|
||||
source: ToolCallSource::Direct,
|
||||
payload,
|
||||
}),
|
||||
@@ -302,7 +393,7 @@ mod tests {
|
||||
arguments: json!({ "message": "hello" }).to_string(),
|
||||
};
|
||||
let (session, turn) = make_session_and_context().await;
|
||||
let handler = McpHandler::new(tool_info("foo", "mcp__foo__", "exec_command"))
|
||||
let handler = McpHandler::new(tool_info("foo", "mcp__foo", "exec_command"))
|
||||
.expect("MCP tool spec should build");
|
||||
|
||||
let invocation = handler
|
||||
@@ -313,7 +404,7 @@ mod tests {
|
||||
cancellation_token: tokio_util::sync::CancellationToken::new(),
|
||||
tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
|
||||
call_id: "call-mcp-rewrite-builtin-like".to_string(),
|
||||
tool_name: codex_tools::ToolName::namespaced("mcp__foo__", "exec_command"),
|
||||
tool_name: codex_tools::ToolName::namespaced("mcp__foo", "exec_command"),
|
||||
source: ToolCallSource::Direct,
|
||||
payload,
|
||||
},
|
||||
@@ -328,7 +419,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mcp_post_tool_use_payload_uses_model_tool_name_args_and_result() {
|
||||
async fn mcp_post_tool_use_payload_uses_prefixed_tool_name_args_and_result() {
|
||||
let payload = ToolPayload::Function {
|
||||
arguments: json!({ "path": "/tmp/notes.txt" }).to_string(),
|
||||
};
|
||||
@@ -352,7 +443,7 @@ mod tests {
|
||||
truncation_policy: codex_utils_output_truncation::TruncationPolicy::Bytes(1024),
|
||||
};
|
||||
let (session, turn) = make_session_and_context().await;
|
||||
let handler = McpHandler::new(tool_info("filesystem", "mcp__filesystem__", "read_file"))
|
||||
let handler = McpHandler::new(tool_info("filesystem", "filesystem", "read_file"))
|
||||
.expect("MCP tool spec should build");
|
||||
let invocation = ToolInvocation {
|
||||
session: session.into(),
|
||||
@@ -360,7 +451,7 @@ mod tests {
|
||||
cancellation_token: tokio_util::sync::CancellationToken::new(),
|
||||
tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
|
||||
call_id: "call-mcp-post".to_string(),
|
||||
tool_name: codex_tools::ToolName::namespaced("mcp__filesystem__", "read_file"),
|
||||
tool_name: codex_tools::ToolName::namespaced("filesystem", "read_file"),
|
||||
source: ToolCallSource::Direct,
|
||||
payload,
|
||||
};
|
||||
|
||||
@@ -197,8 +197,8 @@ mod tests {
|
||||
tools,
|
||||
vec![
|
||||
LoadableToolSpec::Namespace(ResponsesApiNamespace {
|
||||
name: "mcp__calendar__".to_string(),
|
||||
description: "Tools in the mcp__calendar__ namespace.".to_string(),
|
||||
name: "mcp__calendar".to_string(),
|
||||
description: "Tools in the mcp__calendar namespace.".to_string(),
|
||||
tools: vec![
|
||||
ResponsesApiNamespaceTool::Function(ResponsesApiTool {
|
||||
name: "create_event".to_string(),
|
||||
@@ -256,7 +256,7 @@ mod tests {
|
||||
supports_parallel_tool_calls: false,
|
||||
server_origin: None,
|
||||
callable_name: tool_name.to_string(),
|
||||
callable_namespace: format!("mcp__{server_name}__"),
|
||||
callable_namespace: format!("mcp__{server_name}"),
|
||||
namespace_description: None,
|
||||
tool: Tool {
|
||||
name: tool_name.to_string().into(),
|
||||
|
||||
@@ -447,7 +447,7 @@ async fn mcp_and_tool_search_follow_direct_and_deferred_tool_exposure() {
|
||||
let direct_mcp = probe_with(
|
||||
|_| {},
|
||||
ToolPlanInputs {
|
||||
mcp_tools: Some(vec![mcp_tool("direct", "mcp__direct__", "lookup")]),
|
||||
mcp_tools: Some(vec![mcp_tool("direct", "mcp__direct", "lookup")]),
|
||||
..ToolPlanInputs::default()
|
||||
},
|
||||
)
|
||||
@@ -458,12 +458,12 @@ async fn mcp_and_tool_search_follow_direct_and_deferred_tool_exposure() {
|
||||
"read_mcp_resource",
|
||||
]);
|
||||
assert_eq!(
|
||||
direct_mcp.namespace_function_names("mcp__direct__"),
|
||||
direct_mcp.namespace_function_names("mcp__direct"),
|
||||
&["lookup".to_string()]
|
||||
);
|
||||
|
||||
let searchable_mcp = ToolPlanInputs {
|
||||
deferred_mcp_tools: Some(vec![mcp_tool("searchable", "mcp__searchable__", "lookup")]),
|
||||
deferred_mcp_tools: Some(vec![mcp_tool("searchable", "mcp__searchable", "lookup")]),
|
||||
..ToolPlanInputs::default()
|
||||
};
|
||||
|
||||
@@ -512,7 +512,10 @@ async fn mcp_and_tool_search_follow_direct_and_deferred_tool_exposure() {
|
||||
)
|
||||
.await;
|
||||
enabled.assert_visible_contains(&["tool_search"]);
|
||||
enabled.assert_registered_contains(&["tool_search", "mcp__searchable__lookup"]);
|
||||
enabled.assert_registered_contains(&[
|
||||
"tool_search",
|
||||
&ToolName::namespaced("mcp__searchable", "lookup").to_string(),
|
||||
]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -520,18 +523,14 @@ async fn invalid_mcp_tools_are_not_registered() {
|
||||
let plan = probe_with(
|
||||
|_| {},
|
||||
ToolPlanInputs {
|
||||
mcp_tools: Some(vec![invalid_mcp_tool(
|
||||
"invalid",
|
||||
"mcp__invalid__",
|
||||
"lookup",
|
||||
)]),
|
||||
mcp_tools: Some(vec![invalid_mcp_tool("invalid", "mcp__invalid", "lookup")]),
|
||||
..ToolPlanInputs::default()
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
plan.assert_visible_lacks(&["mcp__invalid__"]);
|
||||
plan.assert_registered_lacks(&["mcp__invalid__lookup"]);
|
||||
plan.assert_visible_lacks(&["mcp__invalid"]);
|
||||
plan.assert_registered_lacks(&[&ToolName::namespaced("mcp__invalid", "lookup").to_string()]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user