1use std::collections::{HashMap, HashSet};
2use std::path::PathBuf;
3
4use thiserror::Error;
5
6use crate::cache::{CacheError, write_content_addressed};
7use crate::flags::{
8 IMPORT_INITIALIZE, IMPORT_INITIALIZE_FILE, IMPORT_TOOLS, IMPORT_TOOLS_FILE, long_flag,
9};
10use crate::load::{LoadError, RawFallbackImportSpec, try_load_fallback};
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct RewriteOptions {
14 pub cache_dir: PathBuf,
15 pub file_prefix: String,
16}
17
18impl RewriteOptions {
19 pub fn new(cache_dir: impl Into<PathBuf>) -> Self {
20 Self {
21 cache_dir: cache_dir.into(),
22 file_prefix: "mcp-proxy-import".to_string(),
23 }
24 }
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct RewriteResult {
29 pub args: Vec<String>,
30 pub created_files: Vec<PathBuf>,
31 pub changed: bool,
32}
33
34#[derive(Debug, Error)]
35pub enum RewriteError {
36 #[error("flag --{flag} is missing its value")]
37 MissingValue { flag: &'static str },
38 #[error("flag --{flag} was provided more than once")]
39 DuplicateFlag { flag: &'static str },
40 #[error("fallback import must include both initialize and tools metadata")]
41 IncompletePair,
42 #[error("both inline and file sources were provided for {kind}")]
43 ConflictingSources { kind: &'static str },
44 #[error("cache path is not valid UTF-8: {path}")]
45 NonUtf8CachePath { path: PathBuf },
46 #[error(transparent)]
47 Load(#[from] LoadError),
48 #[error(transparent)]
49 Cache(#[from] CacheError),
50}
51
52#[derive(Debug, Clone)]
53struct ParsedValue {
54 flag_index: usize,
55 value_index: Option<usize>,
56 value: String,
57}
58
59#[derive(Debug, Default)]
60struct ParsedImports {
61 initialize_inline: Option<ParsedValue>,
62 initialize_file: Option<ParsedValue>,
63 tools_inline: Option<ParsedValue>,
64 tools_file: Option<ParsedValue>,
65}
66
67impl ParsedImports {
68 fn insert(
69 slot: &mut Option<ParsedValue>,
70 value: ParsedValue,
71 flag: &'static str,
72 ) -> Result<(), RewriteError> {
73 if slot.is_some() {
74 return Err(RewriteError::DuplicateFlag { flag });
75 }
76 *slot = Some(value);
77 Ok(())
78 }
79
80 fn validate(&self) -> Result<(), RewriteError> {
81 if self.initialize_inline.is_some() && self.initialize_file.is_some() {
82 return Err(RewriteError::ConflictingSources { kind: "initialize" });
83 }
84 if self.tools_inline.is_some() && self.tools_file.is_some() {
85 return Err(RewriteError::ConflictingSources { kind: "tools" });
86 }
87
88 let has_initialize = self.initialize_inline.is_some() || self.initialize_file.is_some();
89 let has_tools = self.tools_inline.is_some() || self.tools_file.is_some();
90 if has_initialize != has_tools {
91 return Err(RewriteError::IncompletePair);
92 }
93 Ok(())
94 }
95}
96
97fn executable_name(command: &str) -> Option<&str> {
98 command
99 .rsplit(['/', '\\'])
100 .next()
101 .filter(|name| !name.is_empty())
102}
103
104pub fn is_mcp_proxy_convert(command: &str, args: &[String]) -> bool {
105 if !matches!(
106 executable_name(command),
107 Some("mcp-proxy" | "mcp-proxy.exe")
108 ) {
109 return false;
110 }
111
112 for arg in args {
113 if arg == "--" {
114 return false;
115 }
116 if matches!(arg.as_str(), "-v" | "--verbose" | "-q" | "--quiet")
117 || (arg.starts_with('-')
118 && !arg.starts_with("--")
119 && arg.len() > 1
120 && arg[1..].chars().all(|ch| matches!(ch, 'v' | 'q')))
121 {
122 continue;
123 }
124 return arg == "convert";
125 }
126 false
127}
128
129fn recognized_flag(arg: &str) -> Option<(&'static str, Option<&str>)> {
130 for name in [
131 IMPORT_INITIALIZE,
132 IMPORT_INITIALIZE_FILE,
133 IMPORT_TOOLS,
134 IMPORT_TOOLS_FILE,
135 ] {
136 let full = long_flag(name);
137 if arg == full {
138 return Some((name, None));
139 }
140 if let Some(value) = arg.strip_prefix(&format!("{full}=")) {
141 return Some((name, Some(value)));
142 }
143 }
144 None
145}
146
147fn parse_imports(args: &[String]) -> Result<ParsedImports, RewriteError> {
148 let mut parsed = ParsedImports::default();
149 let mut index = 0;
150
151 while index < args.len() {
152 if args[index] == "--" {
153 break;
154 }
155 let Some((flag, inline_value)) = recognized_flag(&args[index]) else {
156 index += 1;
157 continue;
158 };
159
160 let (value, value_index) = if let Some(value) = inline_value {
161 (value.to_string(), None)
162 } else {
163 let next = index + 1;
164 let value = args
165 .get(next)
166 .ok_or(RewriteError::MissingValue { flag })?
167 .clone();
168 if recognized_flag(&value).is_some() || value == "--" {
169 return Err(RewriteError::MissingValue { flag });
170 }
171 (value, Some(next))
172 };
173 let parsed_value = ParsedValue {
174 flag_index: index,
175 value_index,
176 value,
177 };
178
179 match flag {
180 IMPORT_INITIALIZE => ParsedImports::insert(
181 &mut parsed.initialize_inline,
182 parsed_value,
183 IMPORT_INITIALIZE,
184 )?,
185 IMPORT_INITIALIZE_FILE => ParsedImports::insert(
186 &mut parsed.initialize_file,
187 parsed_value,
188 IMPORT_INITIALIZE_FILE,
189 )?,
190 IMPORT_TOOLS => {
191 ParsedImports::insert(&mut parsed.tools_inline, parsed_value, IMPORT_TOOLS)?
192 }
193 IMPORT_TOOLS_FILE => {
194 ParsedImports::insert(&mut parsed.tools_file, parsed_value, IMPORT_TOOLS_FILE)?
195 }
196 _ => {}
197 }
198
199 index = value_index.map_or(index + 1, |value_index| value_index + 1);
200 }
201
202 parsed.validate()?;
203 Ok(parsed)
204}
205
206pub fn rewrite_convert_import_args_to_files(
207 command: &str,
208 args: &[String],
209 options: &RewriteOptions,
210) -> Result<RewriteResult, RewriteError> {
211 if !is_mcp_proxy_convert(command, args) {
212 return Ok(RewriteResult {
213 args: args.to_vec(),
214 created_files: Vec::new(),
215 changed: false,
216 });
217 }
218
219 let parsed = parse_imports(args)?;
220 let has_any = parsed.initialize_inline.is_some()
221 || parsed.initialize_file.is_some()
222 || parsed.tools_inline.is_some()
223 || parsed.tools_file.is_some();
224 if !has_any || (parsed.initialize_inline.is_none() && parsed.tools_inline.is_none()) {
225 validate_sources(&parsed)?;
226 return Ok(RewriteResult {
227 args: args.to_vec(),
228 created_files: Vec::new(),
229 changed: false,
230 });
231 }
232
233 validate_sources(&parsed)?;
234 if options.cache_dir.to_str().is_none() {
235 return Err(RewriteError::NonUtf8CachePath {
236 path: options.cache_dir.clone(),
237 });
238 }
239
240 let mut replacements: HashMap<usize, Vec<String>> = HashMap::new();
241 let mut skipped_indexes = HashSet::new();
242 let mut created_files = Vec::new();
243
244 for (kind, file_flag, parsed_value) in [
245 (
246 "initialize",
247 IMPORT_INITIALIZE_FILE,
248 parsed.initialize_inline.as_ref(),
249 ),
250 ("tools", IMPORT_TOOLS_FILE, parsed.tools_inline.as_ref()),
251 ] {
252 let Some(parsed_value) = parsed_value else {
253 continue;
254 };
255 let (path, created) = write_content_addressed(
256 &options.cache_dir,
257 &options.file_prefix,
258 kind,
259 parsed_value.value.as_bytes(),
260 )?;
261 if created {
262 created_files.push(path.clone());
263 }
264 let path_arg = path
265 .to_str()
266 .ok_or_else(|| RewriteError::NonUtf8CachePath { path: path.clone() })?
267 .to_string();
268 replacements.insert(
269 parsed_value.flag_index,
270 vec![long_flag(file_flag), path_arg],
271 );
272 if let Some(value_index) = parsed_value.value_index {
273 skipped_indexes.insert(value_index);
274 }
275 }
276
277 let mut rewritten = Vec::with_capacity(args.len());
278 for (index, arg) in args.iter().enumerate() {
279 if skipped_indexes.contains(&index) {
280 continue;
281 }
282 if let Some(values) = replacements.get(&index) {
283 rewritten.extend(values.iter().cloned());
284 } else {
285 rewritten.push(arg.clone());
286 }
287 }
288
289 Ok(RewriteResult {
290 args: rewritten,
291 created_files,
292 changed: true,
293 })
294}
295
296fn validate_sources(parsed: &ParsedImports) -> Result<(), RewriteError> {
297 let spec = RawFallbackImportSpec {
298 initialize_inline: parsed
299 .initialize_inline
300 .as_ref()
301 .map(|value| value.value.clone()),
302 initialize_file: parsed
303 .initialize_file
304 .as_ref()
305 .map(|value| PathBuf::from(&value.value)),
306 tools_inline: parsed
307 .tools_inline
308 .as_ref()
309 .map(|value| value.value.clone()),
310 tools_file: parsed
311 .tools_file
312 .as_ref()
313 .map(|value| PathBuf::from(&value.value)),
314 };
315 try_load_fallback(&spec)?;
316 Ok(())
317}