Use MCP server instructions in deferred namespace descriptions (#21053)

## Why

MCP servers can provide `instructions` that explain what their tools are
for. Directly exposed MCP namespaces already use those instructions when
a connector description is not available, but deferred `tool_search`
results did not preserve that fallback. The direct path falls back from
connector metadata to server instructions, while the deferred path only
carried `connector_description` and otherwise fell back to generic
namespace text.

That meant a plain MCP server could provide useful model-facing guidance
and still appear as `Tools in the X namespace.` whenever it was
discovered lazily through `tool_search`.

## What changed

- Store one model-facing `namespace_description` on `ToolInfo`, using
connector descriptions for connector-backed tools and server
instructions for plain MCP servers.
- Thread that namespace description through the `tool_search` source
list, search indexing, and returned namespace metadata.
- Add an end-to-end regression test for deferred non-app MCP search
results exposing server instructions as the namespace description.

## Verification

- `cargo test -p codex-tools
search_tool_description_lists_each_mcp_source_once --lib`
- `cargo test -p codex-core --test all
tool_search_uses_non_app_mcp_server_instructions_as_namespace_description`
This commit is contained in:
sayan-oai
2026-05-04 19:36:07 +00:00
committed by GitHub
parent 48402be6fa
commit b9e8df47da
16 changed files with 138 additions and 52 deletions
@@ -40,7 +40,7 @@ fn create_test_tool(server_name: &str, tool_name: &str) -> ToolInfo {
server_name: server_name.to_string(),
callable_name: tool_name.to_string(),
callable_namespace: tool_namespace,
server_instructions: None,
namespace_description: None,
tool: Tool {
name: tool_name.to_string().into(),
title: None,
@@ -55,7 +55,6 @@ fn create_test_tool(server_name: &str, tool_name: &str) -> ToolInfo {
connector_id: None,
connector_name: None,
plugin_display_names: Vec::new(),
connector_description: None,
}
}
+9 -2
View File
@@ -362,16 +362,23 @@ pub(crate) async fn list_tools_for_client_uncached(
tool_def.title = Some(normalized_title);
}
}
let has_connector_metadata = connector_id.is_some()
|| connector_name.is_some()
|| connector_description.is_some();
let namespace_description = if has_connector_metadata {
connector_description
} else {
server_instructions.map(str::to_string)
};
ToolInfo {
server_name: server_name.to_owned(),
callable_name,
callable_namespace,
server_instructions: server_instructions.map(str::to_string),
namespace_description,
tool: tool_def,
connector_id,
connector_name,
plugin_display_names: Vec::new(),
connector_description,
}
})
.collect();
+4 -4
View File
@@ -35,16 +35,16 @@ pub struct ToolInfo {
/// Model-visible namespace used for deferred tool loading.
#[serde(rename = "tool_namespace", alias = "callable_namespace")]
pub callable_namespace: String,
/// Instructions from the MCP server initialize result.
#[serde(default)]
pub server_instructions: Option<String>,
/// Model-visible namespace description.
// Keep the old serialized field name readable for cached ToolInfo values.
#[serde(default, alias = "connector_description")]
pub namespace_description: Option<String>,
/// Raw MCP tool definition; `tool.name` is sent back to the MCP server.
pub tool: Tool,
pub connector_id: Option<String>,
pub connector_name: Option<String>,
#[serde(default)]
pub plugin_display_names: Vec<String>,
pub connector_description: Option<String>,
}
impl ToolInfo {