1use std::collections::HashMap;
9use std::collections::HashSet;
10
11use codex_config::McpServerConfig;
12use codex_protocol::ToolName;
13use rmcp::model::Tool;
14use serde::Deserialize;
15use serde::Serialize;
16use sha1::Digest;
17use sha1::Sha1;
18use tracing::warn;
19
20use crate::mcp::sanitize_responses_api_tool_name;
21
22const LEGACY_MCP_TOOL_NAME_PREFIX: &str = "mcp__";
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct ToolInfo {
26 pub server_name: String,
28 #[serde(default)]
30 pub supports_parallel_tool_calls: bool,
31 #[serde(default)]
33 pub server_origin: Option<String>,
34 #[serde(rename = "tool_name", alias = "callable_name")]
36 pub callable_name: String,
37 #[serde(rename = "tool_namespace", alias = "callable_namespace")]
39 pub callable_namespace: String,
40 #[serde(default, alias = "connector_description")]
43 pub namespace_description: Option<String>,
44 pub tool: Tool,
46 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
50 pub openai_file_input_optional_fields: HashMap<String, Vec<String>>,
51 pub connector_id: Option<String>,
52 pub connector_name: Option<String>,
53 #[serde(default)]
54 pub plugin_display_names: Vec<String>,
55}
56
57impl ToolInfo {
58 pub fn canonical_tool_name(&self) -> ToolName {
59 ToolName::namespaced(self.callable_namespace.clone(), self.callable_name.clone())
60 }
61}
62
63#[derive(Default, Clone)]
67pub(crate) struct ToolFilter {
68 pub(crate) enabled: Option<HashSet<String>>,
69 pub(crate) disabled: HashSet<String>,
70}
71
72impl ToolFilter {
73 pub(crate) fn from_config(cfg: &McpServerConfig) -> Self {
74 let enabled = cfg
75 .enabled_tools
76 .as_ref()
77 .map(|tools| tools.iter().cloned().collect::<HashSet<_>>());
78 let disabled = cfg
79 .disabled_tools
80 .as_ref()
81 .map(|tools| tools.iter().cloned().collect::<HashSet<_>>())
82 .unwrap_or_default();
83
84 Self { enabled, disabled }
85 }
86
87 pub(crate) fn allows(&self, tool_name: &str) -> bool {
88 if let Some(enabled) = &self.enabled
89 && !enabled.contains(tool_name)
90 {
91 return false;
92 }
93
94 !self.disabled.contains(tool_name)
95 }
96}
97
98pub(crate) fn filter_tools(tools: Vec<ToolInfo>, filter: &ToolFilter) -> Vec<ToolInfo> {
99 tools
100 .into_iter()
101 .filter(|tool| filter.allows(&tool.tool.name))
102 .collect()
103}
104
105pub(crate) fn normalize_tools_for_model_with_prefix<I>(
114 tools: I,
115 prefix_mcp_tool_names: bool,
116) -> Vec<ToolInfo>
117where
118 I: IntoIterator<Item = ToolInfo>,
119{
120 let mut seen_raw_names = HashSet::new();
121 let mut candidates = Vec::new();
122 for tool in tools {
123 let raw_namespace_identity = format!(
124 "{}\0{}\0{}",
125 tool.server_name,
126 tool.callable_namespace,
127 tool.connector_id.as_deref().unwrap_or_default()
128 );
129 let raw_tool_identity = format!(
130 "{}\0{}\0{}",
131 raw_namespace_identity, tool.callable_name, tool.tool.name
132 );
133 if !seen_raw_names.insert(raw_tool_identity.clone()) {
134 warn!("skipping duplicated tool {}", tool.tool.name);
135 continue;
136 }
137
138 let callable_namespace = callable_namespace_with_prefix(
139 &sanitize_responses_api_tool_name(&tool.callable_namespace),
140 prefix_mcp_tool_names,
141 );
142
143 candidates.push(CallableToolCandidate {
144 callable_namespace,
145 callable_name: sanitize_responses_api_tool_name(&tool.callable_name),
146 raw_namespace_identity,
147 raw_tool_identity,
148 tool,
149 });
150 }
151
152 let mut namespace_identities_by_base = HashMap::<String, HashSet<String>>::new();
153 for candidate in &candidates {
154 namespace_identities_by_base
155 .entry(candidate.callable_namespace.clone())
156 .or_default()
157 .insert(candidate.raw_namespace_identity.clone());
158 }
159 let colliding_namespaces = namespace_identities_by_base
160 .into_iter()
161 .filter_map(|(namespace, identities)| (identities.len() > 1).then_some(namespace))
162 .collect::<HashSet<_>>();
163 for candidate in &mut candidates {
164 if colliding_namespaces.contains(&candidate.callable_namespace) {
165 candidate.callable_namespace = append_namespace_hash_suffix(
166 &candidate.callable_namespace,
167 &candidate.raw_namespace_identity,
168 );
169 }
170 }
171
172 let mut tool_identities_by_base = HashMap::<(String, String), HashSet<String>>::new();
173 for candidate in &candidates {
174 tool_identities_by_base
175 .entry((
176 candidate.callable_namespace.clone(),
177 candidate.callable_name.clone(),
178 ))
179 .or_default()
180 .insert(candidate.raw_tool_identity.clone());
181 }
182 let colliding_tools = tool_identities_by_base
183 .into_iter()
184 .filter_map(|(key, identities)| (identities.len() > 1).then_some(key))
185 .collect::<HashSet<_>>();
186 for candidate in &mut candidates {
187 if colliding_tools.contains(&(
188 candidate.callable_namespace.clone(),
189 candidate.callable_name.clone(),
190 )) {
191 candidate.callable_name =
192 append_hash_suffix(&candidate.callable_name, &candidate.raw_tool_identity);
193 }
194 }
195
196 candidates.sort_by(|left, right| left.raw_tool_identity.cmp(&right.raw_tool_identity));
197
198 let mut used_names = HashSet::new();
199 let mut model_tools = Vec::new();
200 for mut candidate in candidates {
201 let (callable_namespace, callable_name) = unique_callable_parts(
202 &candidate.callable_namespace,
203 &candidate.callable_name,
204 &candidate.raw_tool_identity,
205 &mut used_names,
206 MCP_TOOL_NAME_DELIMITER.len(),
207 );
208 candidate.tool.callable_namespace = callable_namespace;
209 candidate.tool.callable_name = callable_name;
210 model_tools.push(candidate.tool);
211 }
212 model_tools
213}
214
215#[derive(Debug)]
216struct CallableToolCandidate {
217 tool: ToolInfo,
218 raw_namespace_identity: String,
219 raw_tool_identity: String,
220 callable_namespace: String,
221 callable_name: String,
222}
223
224const MCP_TOOL_NAME_DELIMITER: &str = "__";
225const MAX_TOOL_NAME_LENGTH: usize = 64;
226const CALLABLE_NAME_HASH_LEN: usize = 12;
227fn callable_namespace_with_prefix(namespace: &str, prefix_mcp_tool_names: bool) -> String {
228 if !prefix_mcp_tool_names || namespace.starts_with(LEGACY_MCP_TOOL_NAME_PREFIX) {
229 namespace.to_string()
230 } else {
231 format!("{LEGACY_MCP_TOOL_NAME_PREFIX}{namespace}")
232 }
233}
234
235fn sha1_hex(s: &str) -> String {
236 let mut hasher = Sha1::new();
237 hasher.update(s.as_bytes());
238 let sha1 = hasher.finalize();
239 format!("{sha1:x}")
240}
241
242fn callable_name_hash_suffix(raw_identity: &str) -> String {
243 let hash = sha1_hex(raw_identity);
244 format!("_{}", &hash[..CALLABLE_NAME_HASH_LEN])
245}
246
247fn append_hash_suffix(value: &str, raw_identity: &str) -> String {
248 format!("{value}{}", callable_name_hash_suffix(raw_identity))
249}
250
251fn append_namespace_hash_suffix(namespace: &str, raw_identity: &str) -> String {
252 if let Some(namespace) = namespace.strip_suffix(MCP_TOOL_NAME_DELIMITER) {
253 format!(
254 "{}{}{}",
255 namespace,
256 callable_name_hash_suffix(raw_identity),
257 MCP_TOOL_NAME_DELIMITER
258 )
259 } else {
260 append_hash_suffix(namespace, raw_identity)
261 }
262}
263
264fn truncate_name(value: &str, max_len: usize) -> String {
265 value.chars().take(max_len).collect()
266}
267
268fn fit_callable_parts_with_hash(
269 namespace: &str,
270 tool_name: &str,
271 raw_identity: &str,
272 reserved_len: usize,
273) -> (String, String) {
274 let suffix = callable_name_hash_suffix(raw_identity);
275 let max_tool_len = MAX_TOOL_NAME_LENGTH.saturating_sub(namespace.len() + reserved_len);
276 if max_tool_len >= suffix.len() {
277 let prefix_len = max_tool_len - suffix.len();
278 return (
279 namespace.to_string(),
280 format!("{}{}", truncate_name(tool_name, prefix_len), suffix),
281 );
282 }
283
284 let max_namespace_len = MAX_TOOL_NAME_LENGTH.saturating_sub(suffix.len() + reserved_len);
285 (truncate_name(namespace, max_namespace_len), suffix)
286}
287
288fn unique_callable_parts(
289 namespace: &str,
290 tool_name: &str,
291 raw_identity: &str,
292 used_names: &mut HashSet<String>,
293 reserved_len: usize,
294) -> (String, String) {
295 let model_name = format!("{namespace}{tool_name}");
296 if model_name.len() + reserved_len <= MAX_TOOL_NAME_LENGTH && used_names.insert(model_name) {
297 return (namespace.to_string(), tool_name.to_string());
298 }
299
300 let mut attempt = 0_u32;
301 loop {
302 let hash_input = if attempt == 0 {
303 raw_identity.to_string()
304 } else {
305 format!("{raw_identity}\0{attempt}")
306 };
307 let (namespace, tool_name) =
308 fit_callable_parts_with_hash(namespace, tool_name, &hash_input, reserved_len);
309 let model_name = format!("{namespace}{tool_name}");
310 if used_names.insert(model_name) {
311 return (namespace, tool_name);
312 }
313 attempt = attempt.saturating_add(1);
314 }
315}