mirror of
https://github.com/openai/codex.git
synced 2026-04-18 03:34:50 +00:00
Compare commits
7 Commits
dev/shaqay
...
dev/mzeng/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eda59fc8f0 | ||
|
|
86f6eb8e45 | ||
|
|
41052984fd | ||
|
|
1c7bf9d35a | ||
|
|
1e376a90d3 | ||
|
|
c51c16fca9 | ||
|
|
9d85fa81b3 |
@@ -1736,6 +1736,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
|
||||
@@ -9184,6 +9184,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
|
||||
@@ -6007,6 +6007,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
|
||||
@@ -294,6 +294,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
|
||||
@@ -294,6 +294,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
|
||||
@@ -430,6 +430,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
|
||||
@@ -518,6 +518,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
|
||||
@@ -456,6 +456,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
|
||||
@@ -456,6 +456,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
|
||||
@@ -456,6 +456,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
|
||||
@@ -518,6 +518,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
|
||||
@@ -456,6 +456,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
|
||||
@@ -518,6 +518,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
|
||||
@@ -456,6 +456,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
|
||||
@@ -456,6 +456,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
|
||||
@@ -430,6 +430,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
|
||||
@@ -430,6 +430,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
|
||||
@@ -430,6 +430,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { JsonValue } from "../serde_json/JsonValue";
|
||||
|
||||
export type McpToolCallResult = { content: Array<JsonValue>, structuredContent: JsonValue | null, };
|
||||
export type McpToolCallResult = { content: Array<JsonValue>, structuredContent: JsonValue | null, _meta: JsonValue | null, };
|
||||
|
||||
@@ -520,6 +520,7 @@ impl ThreadHistoryBuilder {
|
||||
Some(McpToolCallResult {
|
||||
content: value.content.clone(),
|
||||
structured_content: value.structured_content.clone(),
|
||||
meta: value.meta.clone(),
|
||||
}),
|
||||
None,
|
||||
),
|
||||
@@ -1167,6 +1168,7 @@ mod tests {
|
||||
use codex_protocol::items::TurnItem as CoreTurnItem;
|
||||
use codex_protocol::items::UserMessageItem as CoreUserMessageItem;
|
||||
use codex_protocol::items::build_hook_prompt_message;
|
||||
use codex_protocol::mcp::CallToolResult;
|
||||
use codex_protocol::models::MessagePhase as CoreMessagePhase;
|
||||
use codex_protocol::models::WebSearchAction as CoreWebSearchAction;
|
||||
use codex_protocol::parse_command::ParsedCommand;
|
||||
@@ -1861,6 +1863,67 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconstructs_mcp_tool_result_meta_from_persisted_completion_events() {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".into(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
}),
|
||||
EventMsg::McpToolCallEnd(McpToolCallEndEvent {
|
||||
call_id: "mcp-1".into(),
|
||||
invocation: McpInvocation {
|
||||
server: "docs".into(),
|
||||
tool: "lookup".into(),
|
||||
arguments: Some(serde_json::json!({"id":"123"})),
|
||||
},
|
||||
duration: Duration::from_millis(8),
|
||||
result: Ok(CallToolResult {
|
||||
content: vec![serde_json::json!({
|
||||
"type": "text",
|
||||
"text": "result"
|
||||
})],
|
||||
structured_content: Some(serde_json::json!({"id":"123"})),
|
||||
is_error: Some(false),
|
||||
meta: Some(serde_json::json!({
|
||||
"ui/resourceUri": "ui://widget/lookup.html"
|
||||
})),
|
||||
}),
|
||||
}),
|
||||
];
|
||||
|
||||
let items = events
|
||||
.into_iter()
|
||||
.map(RolloutItem::EventMsg)
|
||||
.collect::<Vec<_>>();
|
||||
let turns = build_turns_from_rollout_items(&items);
|
||||
assert_eq!(turns.len(), 1);
|
||||
assert_eq!(
|
||||
turns[0].items[0],
|
||||
ThreadItem::McpToolCall {
|
||||
id: "mcp-1".into(),
|
||||
server: "docs".into(),
|
||||
tool: "lookup".into(),
|
||||
status: McpToolCallStatus::Completed,
|
||||
arguments: serde_json::json!({"id":"123"}),
|
||||
result: Some(McpToolCallResult {
|
||||
content: vec![serde_json::json!({
|
||||
"type": "text",
|
||||
"text": "result"
|
||||
})],
|
||||
structured_content: Some(serde_json::json!({"id":"123"})),
|
||||
meta: Some(serde_json::json!({
|
||||
"ui/resourceUri": "ui://widget/lookup.html"
|
||||
})),
|
||||
}),
|
||||
error: None,
|
||||
duration_ms: Some(8),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconstructs_dynamic_tool_items_from_request_and_response_events() {
|
||||
let events = vec![
|
||||
|
||||
@@ -5048,6 +5048,9 @@ pub struct McpToolCallResult {
|
||||
// representations). Using `JsonValue` keeps the payload wire-shaped and easy to export.
|
||||
pub content: Vec<JsonValue>,
|
||||
pub structured_content: Option<JsonValue>,
|
||||
#[serde(rename = "_meta")]
|
||||
#[ts(rename = "_meta")]
|
||||
pub meta: Option<JsonValue>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
|
||||
@@ -2843,6 +2843,7 @@ async fn construct_mcp_tool_call_end_notification(
|
||||
Some(McpToolCallResult {
|
||||
content: value.content.clone(),
|
||||
structured_content: value.structured_content.clone(),
|
||||
meta: value.meta.clone(),
|
||||
}),
|
||||
None,
|
||||
),
|
||||
@@ -4233,7 +4234,9 @@ mod tests {
|
||||
content: content.clone(),
|
||||
is_error: Some(false),
|
||||
structured_content: None,
|
||||
meta: None,
|
||||
meta: Some(serde_json::json!({
|
||||
"ui/resourceUri": "ui://widget/list-resources.html"
|
||||
})),
|
||||
};
|
||||
|
||||
let end_event = McpToolCallEndEvent {
|
||||
@@ -4268,6 +4271,9 @@ mod tests {
|
||||
result: Some(McpToolCallResult {
|
||||
content,
|
||||
structured_content: None,
|
||||
meta: Some(serde_json::json!({
|
||||
"ui/resourceUri": "ui://widget/list-resources.html"
|
||||
})),
|
||||
}),
|
||||
error: None,
|
||||
duration_ms: Some(0),
|
||||
|
||||
@@ -251,9 +251,8 @@ use codex_login::run_login_server;
|
||||
use codex_mcp::mcp::McpSnapshotDetail;
|
||||
use codex_mcp::mcp::auth::discover_supported_scopes;
|
||||
use codex_mcp::mcp::auth::resolve_oauth_scopes;
|
||||
use codex_mcp::mcp::collect_mcp_snapshot_with_detail;
|
||||
use codex_mcp::mcp::collect_mcp_server_status_snapshot_with_detail;
|
||||
use codex_mcp::mcp::effective_mcp_servers;
|
||||
use codex_mcp::mcp::qualified_mcp_tool_name_prefix;
|
||||
use codex_models_manager::collaboration_mode_presets::CollaborationModesConfig;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
@@ -5183,55 +5182,14 @@ impl CodexMessageProcessor {
|
||||
McpServerStatusDetail::ToolsAndAuthOnly => McpSnapshotDetail::ToolsAndAuthOnly,
|
||||
};
|
||||
|
||||
let snapshot = collect_mcp_snapshot_with_detail(
|
||||
let snapshot = collect_mcp_server_status_snapshot_with_detail(
|
||||
&mcp_config,
|
||||
auth.as_ref(),
|
||||
request_id.request_id.to_string(),
|
||||
detail,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Rebuild the tool list per original server name instead of using
|
||||
// `group_tools_by_server()`: qualified tool names are sanitized for the
|
||||
// Responses API, so a config key like `some-server` is encoded as the
|
||||
// `mcp__some_server__` prefix. Matching with the original server name's
|
||||
// sanitized prefix preserves `/mcp` output for hyphenated names.
|
||||
let effective_servers = effective_mcp_servers(&mcp_config, auth.as_ref());
|
||||
let mut sanitized_prefix_counts = HashMap::<String, usize>::new();
|
||||
for name in effective_servers.keys() {
|
||||
let prefix = qualified_mcp_tool_name_prefix(name);
|
||||
*sanitized_prefix_counts.entry(prefix).or_default() += 1;
|
||||
}
|
||||
let tools_by_server = effective_servers
|
||||
.keys()
|
||||
.map(|name| {
|
||||
let prefix = qualified_mcp_tool_name_prefix(name);
|
||||
// If multiple server names normalize to the same prefix, the
|
||||
// qualified tool namespace is ambiguous (for example
|
||||
// `some-server` and `some_server` both become
|
||||
// `mcp__some_server__`). In that case, avoid attributing the
|
||||
// same tools to multiple servers.
|
||||
let tools = if sanitized_prefix_counts
|
||||
.get(&prefix)
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
== 1
|
||||
{
|
||||
snapshot
|
||||
.tools
|
||||
.iter()
|
||||
.filter_map(|(qualified_name, tool)| {
|
||||
qualified_name
|
||||
.strip_prefix(&prefix)
|
||||
.map(|tool_name| (tool_name.to_string(), tool.clone()))
|
||||
})
|
||||
.collect::<HashMap<_, _>>()
|
||||
} else {
|
||||
HashMap::new()
|
||||
};
|
||||
(name.clone(), tools)
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let mut server_names: Vec<String> = config
|
||||
.mcp_servers
|
||||
@@ -5283,7 +5241,11 @@ impl CodexMessageProcessor {
|
||||
.iter()
|
||||
.map(|name| McpServerStatus {
|
||||
name: name.clone(),
|
||||
tools: tools_by_server.get(name).cloned().unwrap_or_default(),
|
||||
tools: snapshot
|
||||
.tools_by_server
|
||||
.get(name)
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
resources: snapshot.resources.get(name).cloned().unwrap_or_default(),
|
||||
resource_templates: snapshot
|
||||
.resource_templates
|
||||
|
||||
@@ -19,6 +19,7 @@ use codex_protocol::mcp::Resource;
|
||||
use codex_protocol::mcp::ResourceTemplate;
|
||||
use codex_protocol::mcp::Tool;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::McpAuthStatus;
|
||||
use codex_protocol::protocol::McpListToolsResponseEvent;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use serde_json::Value;
|
||||
@@ -26,6 +27,7 @@ use serde_json::Value;
|
||||
use crate::mcp::auth::compute_auth_statuses;
|
||||
use crate::mcp_connection_manager::McpConnectionManager;
|
||||
use crate::mcp_connection_manager::SandboxState;
|
||||
use crate::mcp_connection_manager::ToolInfo;
|
||||
use crate::mcp_connection_manager::codex_apps_tools_cache_key;
|
||||
pub type McpManager = McpConnectionManager;
|
||||
|
||||
@@ -292,6 +294,14 @@ pub fn tool_plugin_provenance(config: &McpConfig) -> ToolPluginProvenance {
|
||||
ToolPluginProvenance::from_capability_summaries(&config.plugin_capability_summaries)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct McpServerStatusSnapshot {
|
||||
pub tools_by_server: HashMap<String, HashMap<String, Tool>>,
|
||||
pub resources: HashMap<String, Vec<Resource>>,
|
||||
pub resource_templates: HashMap<String, Vec<ResourceTemplate>>,
|
||||
pub auth_statuses: HashMap<String, McpAuthStatus>,
|
||||
}
|
||||
|
||||
pub async fn collect_mcp_snapshot(
|
||||
config: &McpConfig,
|
||||
auth: Option<&CodexAuth>,
|
||||
@@ -357,33 +367,119 @@ pub async fn collect_mcp_snapshot_with_detail(
|
||||
snapshot
|
||||
}
|
||||
|
||||
pub fn split_qualified_tool_name(qualified_name: &str) -> Option<(String, String)> {
|
||||
let mut parts = qualified_name.split(MCP_TOOL_NAME_DELIMITER);
|
||||
let prefix = parts.next()?;
|
||||
if prefix != MCP_TOOL_NAME_PREFIX {
|
||||
return None;
|
||||
pub async fn collect_mcp_server_status_snapshot_with_detail(
|
||||
config: &McpConfig,
|
||||
auth: Option<&CodexAuth>,
|
||||
submit_id: String,
|
||||
detail: McpSnapshotDetail,
|
||||
) -> McpServerStatusSnapshot {
|
||||
let mcp_servers = effective_mcp_servers(config, auth);
|
||||
let tool_plugin_provenance = tool_plugin_provenance(config);
|
||||
if mcp_servers.is_empty() {
|
||||
return McpServerStatusSnapshot::default();
|
||||
}
|
||||
let server_name = parts.next()?;
|
||||
let tool_name: String = parts.collect::<Vec<_>>().join(MCP_TOOL_NAME_DELIMITER);
|
||||
if tool_name.is_empty() {
|
||||
return None;
|
||||
|
||||
let mut sanitized_prefix_counts = HashMap::<String, usize>::new();
|
||||
for name in mcp_servers.keys() {
|
||||
let prefix = qualified_mcp_tool_name_prefix(name);
|
||||
*sanitized_prefix_counts.entry(prefix).or_default() += 1;
|
||||
}
|
||||
Some((server_name.to_string(), tool_name))
|
||||
|
||||
let auth_status_entries =
|
||||
compute_auth_statuses(mcp_servers.iter(), config.mcp_oauth_credentials_store_mode).await;
|
||||
|
||||
let (tx_event, rx_event) = unbounded();
|
||||
drop(rx_event);
|
||||
|
||||
let sandbox_state = SandboxState {
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(),
|
||||
sandbox_cwd: env::current_dir().unwrap_or_else(|_| PathBuf::from("/")),
|
||||
use_legacy_landlock: config.use_legacy_landlock,
|
||||
};
|
||||
|
||||
let (mcp_connection_manager, cancel_token) = McpConnectionManager::new(
|
||||
&mcp_servers,
|
||||
config.mcp_oauth_credentials_store_mode,
|
||||
auth_status_entries.clone(),
|
||||
&config.approval_policy,
|
||||
submit_id,
|
||||
tx_event,
|
||||
sandbox_state,
|
||||
config.codex_home.clone(),
|
||||
codex_apps_tools_cache_key(auth),
|
||||
tool_plugin_provenance,
|
||||
)
|
||||
.await;
|
||||
|
||||
let snapshot = collect_mcp_server_status_snapshot_from_manager_with_detail(
|
||||
&mcp_connection_manager,
|
||||
auth_status_entries,
|
||||
detail,
|
||||
&sanitized_prefix_counts,
|
||||
)
|
||||
.await;
|
||||
|
||||
cancel_token.cancel();
|
||||
|
||||
snapshot
|
||||
}
|
||||
|
||||
pub fn group_tools_by_server(
|
||||
tools: &HashMap<String, Tool>,
|
||||
) -> HashMap<String, HashMap<String, Tool>> {
|
||||
let mut grouped = HashMap::new();
|
||||
for (qualified_name, tool) in tools {
|
||||
if let Some((server_name, tool_name)) = split_qualified_tool_name(qualified_name) {
|
||||
grouped
|
||||
async fn collect_mcp_server_status_snapshot_from_manager_with_detail(
|
||||
mcp_connection_manager: &McpConnectionManager,
|
||||
auth_status_entries: HashMap<String, crate::mcp::auth::McpAuthStatusEntry>,
|
||||
detail: McpSnapshotDetail,
|
||||
sanitized_prefix_counts: &HashMap<String, usize>,
|
||||
) -> McpServerStatusSnapshot {
|
||||
let (tools, resources, resource_templates) = tokio::join!(
|
||||
mcp_connection_manager.list_all_tools(),
|
||||
async {
|
||||
if detail.include_resources() {
|
||||
mcp_connection_manager.list_all_resources().await
|
||||
} else {
|
||||
HashMap::new()
|
||||
}
|
||||
},
|
||||
async {
|
||||
if detail.include_resources() {
|
||||
mcp_connection_manager.list_all_resource_templates().await
|
||||
} else {
|
||||
HashMap::new()
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let mut tools_by_server = HashMap::new();
|
||||
for tool_info in tools.into_values() {
|
||||
let prefix = qualified_mcp_tool_name_prefix(&tool_info.server_name);
|
||||
if sanitized_prefix_counts
|
||||
.get(&prefix)
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
!= 1
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let tool_name = mcp_server_status_tool_name(&tool_info);
|
||||
let server_name = tool_info.server_name.clone();
|
||||
if let Some(tool) = convert_mcp_tool(tool_info.tool) {
|
||||
tools_by_server
|
||||
.entry(server_name)
|
||||
.or_insert_with(HashMap::new)
|
||||
.insert(tool_name, tool.clone());
|
||||
.insert(tool_name, tool);
|
||||
}
|
||||
}
|
||||
grouped
|
||||
|
||||
McpServerStatusSnapshot {
|
||||
tools_by_server,
|
||||
resources: convert_mcp_resources(resources),
|
||||
resource_templates: convert_mcp_resource_templates(resource_templates),
|
||||
auth_statuses: auth_status_entries
|
||||
.iter()
|
||||
.map(|(name, entry)| (name.clone(), entry.auth_status))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn collect_mcp_snapshot_from_manager(
|
||||
@@ -428,22 +524,52 @@ pub async fn collect_mcp_snapshot_from_manager_with_detail(
|
||||
|
||||
let tools = tools
|
||||
.into_iter()
|
||||
.filter_map(|(name, tool)| match serde_json::to_value(tool.tool) {
|
||||
Ok(value) => match Tool::from_mcp_value(value) {
|
||||
Ok(tool) => Some((name, tool)),
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to convert MCP tool '{name}': {err}");
|
||||
None
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to serialize MCP tool '{name}': {err}");
|
||||
None
|
||||
}
|
||||
})
|
||||
.filter_map(|(name, tool_info)| convert_mcp_tool(tool_info.tool).map(|tool| (name, tool)))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let resources = resources
|
||||
McpListToolsResponseEvent {
|
||||
tools,
|
||||
resources: convert_mcp_resources(resources),
|
||||
resource_templates: convert_mcp_resource_templates(resource_templates),
|
||||
auth_statuses,
|
||||
}
|
||||
}
|
||||
|
||||
fn mcp_server_status_tool_name(tool: &ToolInfo) -> String {
|
||||
if tool.server_name != CODEX_APPS_MCP_SERVER_NAME {
|
||||
return tool.tool_name.clone();
|
||||
}
|
||||
|
||||
let codex_apps_prefix = qualified_mcp_tool_name_prefix(CODEX_APPS_MCP_SERVER_NAME);
|
||||
if let Some(connector_namespace) = tool.tool_namespace.strip_prefix(&codex_apps_prefix) {
|
||||
return format!("{}{}", connector_namespace, tool.tool_name);
|
||||
}
|
||||
|
||||
tool.tool_name.clone()
|
||||
}
|
||||
|
||||
fn convert_mcp_tool(tool: rmcp::model::Tool) -> Option<Tool> {
|
||||
let value = match serde_json::to_value(tool) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to serialize MCP tool: {err}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
match Tool::from_mcp_value(value) {
|
||||
Ok(tool) => Some(tool),
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to convert MCP tool: {err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_mcp_resources(
|
||||
resources: HashMap<String, Vec<rmcp::model::Resource>>,
|
||||
) -> HashMap<String, Vec<Resource>> {
|
||||
resources
|
||||
.into_iter()
|
||||
.map(|(name, resources)| {
|
||||
let resources = resources
|
||||
@@ -476,9 +602,13 @@ pub async fn collect_mcp_snapshot_from_manager_with_detail(
|
||||
.collect::<Vec<_>>();
|
||||
(name, resources)
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
.collect()
|
||||
}
|
||||
|
||||
let resource_templates = resource_templates
|
||||
fn convert_mcp_resource_templates(
|
||||
resource_templates: HashMap<String, Vec<rmcp::model::ResourceTemplate>>,
|
||||
) -> HashMap<String, Vec<ResourceTemplate>> {
|
||||
resource_templates
|
||||
.into_iter()
|
||||
.map(|(name, templates)| {
|
||||
let templates = templates
|
||||
@@ -512,14 +642,7 @@ pub async fn collect_mcp_snapshot_from_manager_with_detail(
|
||||
.collect::<Vec<_>>();
|
||||
(name, templates)
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
McpListToolsResponseEvent {
|
||||
tools,
|
||||
resources,
|
||||
resource_templates,
|
||||
auth_statuses,
|
||||
}
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -5,8 +5,10 @@ use codex_plugin::AppConnectorId;
|
||||
use codex_plugin::PluginCapabilitySummary;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rmcp::model::JsonObject;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn test_mcp_config(codex_home: PathBuf) -> McpConfig {
|
||||
McpConfig {
|
||||
@@ -25,27 +27,29 @@ fn test_mcp_config(codex_home: PathBuf) -> McpConfig {
|
||||
}
|
||||
}
|
||||
|
||||
fn make_tool(name: &str) -> Tool {
|
||||
Tool {
|
||||
name: name.to_string(),
|
||||
title: None,
|
||||
description: None,
|
||||
input_schema: serde_json::json!({"type": "object", "properties": {}}),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
fn make_tool_info(server_name: &str, tool_name: &str, tool_namespace: &str) -> ToolInfo {
|
||||
ToolInfo {
|
||||
server_name: server_name.to_string(),
|
||||
tool_name: tool_name.to_string(),
|
||||
tool_namespace: tool_namespace.to_string(),
|
||||
tool: rmcp::model::Tool {
|
||||
name: tool_name.to_string().into(),
|
||||
title: None,
|
||||
description: None,
|
||||
input_schema: Arc::new(JsonObject::default()),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
execution: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
},
|
||||
connector_id: None,
|
||||
connector_name: None,
|
||||
plugin_display_names: Vec::new(),
|
||||
connector_description: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_qualified_tool_name_returns_server_and_tool() {
|
||||
assert_eq!(
|
||||
split_qualified_tool_name("mcp__alpha__do_thing"),
|
||||
Some(("alpha".to_string(), "do_thing".to_string()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qualified_mcp_tool_name_prefix_sanitizes_server_names_without_lowercasing() {
|
||||
assert_eq!(
|
||||
@@ -55,33 +59,27 @@ fn qualified_mcp_tool_name_prefix_sanitizes_server_names_without_lowercasing() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_qualified_tool_name_rejects_invalid_names() {
|
||||
assert_eq!(split_qualified_tool_name("other__alpha__do_thing"), None);
|
||||
assert_eq!(split_qualified_tool_name("mcp__alpha__"), None);
|
||||
fn mcp_server_status_tool_name_preserves_hyphenated_mcp_tool_names() {
|
||||
let tool_info = make_tool_info("music-studio", "play-live-pattern", "music-studio");
|
||||
|
||||
assert_eq!(
|
||||
mcp_server_status_tool_name(&tool_info),
|
||||
"play-live-pattern".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_tools_by_server_strips_prefix_and_groups() {
|
||||
let mut tools = HashMap::new();
|
||||
tools.insert("mcp__alpha__do_thing".to_string(), make_tool("do_thing"));
|
||||
tools.insert(
|
||||
"mcp__alpha__nested__op".to_string(),
|
||||
make_tool("nested__op"),
|
||||
fn mcp_server_status_tool_name_includes_codex_apps_connector_namespace() {
|
||||
let tool_info = make_tool_info(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"_property_search",
|
||||
"mcp__codex_apps__zillow",
|
||||
);
|
||||
tools.insert("mcp__beta__do_other".to_string(), make_tool("do_other"));
|
||||
|
||||
let mut expected_alpha = HashMap::new();
|
||||
expected_alpha.insert("do_thing".to_string(), make_tool("do_thing"));
|
||||
expected_alpha.insert("nested__op".to_string(), make_tool("nested__op"));
|
||||
|
||||
let mut expected_beta = HashMap::new();
|
||||
expected_beta.insert("do_other".to_string(), make_tool("do_other"));
|
||||
|
||||
let mut expected = HashMap::new();
|
||||
expected.insert("alpha".to_string(), expected_alpha);
|
||||
expected.insert("beta".to_string(), expected_beta);
|
||||
|
||||
assert_eq!(group_tools_by_server(&tools), expected);
|
||||
assert_eq!(
|
||||
mcp_server_status_tool_name(&tool_info),
|
||||
"zillow_property_search".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -482,6 +482,7 @@ fn mcp_tool_call_begin_and_end_emit_item_events() {
|
||||
result: Some(McpToolCallResult {
|
||||
content: Vec::new(),
|
||||
structured_content: None,
|
||||
meta: None,
|
||||
}),
|
||||
error: None,
|
||||
duration_ms: Some(1_000),
|
||||
@@ -613,6 +614,7 @@ fn mcp_tool_call_defaults_arguments_and_preserves_structured_content() {
|
||||
"text": "done",
|
||||
})],
|
||||
structured_content: Some(json!({ "status": "ok" })),
|
||||
meta: None,
|
||||
}),
|
||||
error: None,
|
||||
duration_ms: Some(10),
|
||||
|
||||
Reference in New Issue
Block a user