| 404 | } |
| 405 | |
| 406 | fn read_optional_json_object( |
| 407 | path: &Path, |
| 408 | ) -> Result<Option<BTreeMap<String, JsonValue>>, ConfigError> { |
| 409 | let is_legacy_config = path.file_name().and_then(|name| name.to_str()) == Some(".claude.json"); |
| 410 | let contents = match fs::read_to_string(path) { |
| 411 | Ok(contents) => contents, |
| 412 | Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None), |
| 413 | Err(error) => return Err(ConfigError::Io(error)), |
| 414 | }; |
| 415 | |
| 416 | if contents.trim().is_empty() { |
| 417 | return Ok(Some(BTreeMap::new())); |
| 418 | } |
| 419 | |
| 420 | let parsed = match JsonValue::parse(&contents) { |
| 421 | Ok(parsed) => parsed, |
| 422 | Err(error) if is_legacy_config => return Ok(None), |
| 423 | Err(error) => return Err(ConfigError::Parse(format!("{}: {error}", path.display()))), |
| 424 | }; |
| 425 | let Some(object) = parsed.as_object() else { |
| 426 | if is_legacy_config { |
| 427 | return Ok(None); |
| 428 | } |
| 429 | return Err(ConfigError::Parse(format!( |
| 430 | "{}: top-level settings value must be a JSON object", |
| 431 | path.display() |
| 432 | ))); |
| 433 | }; |
| 434 | Ok(Some(object.clone())) |
| 435 | } |
| 436 | |
| 437 | fn merge_mcp_servers( |
| 438 | target: &mut BTreeMap<String, ScopedMcpServerConfig>, |