mcp-proxy-args 0.2.3

Argument loading and file rewriting utilities for mcp-proxy convert
Documentation
use std::fs;
use std::path::PathBuf;

use serde_json::Value;
use thiserror::Error;

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RawFallbackImportSpec {
    pub initialize_inline: Option<String>,
    pub initialize_file: Option<PathBuf>,
    pub tools_inline: Option<String>,
    pub tools_file: Option<PathBuf>,
}

impl RawFallbackImportSpec {
    pub fn has_any(&self) -> bool {
        self.initialize_inline.is_some()
            || self.initialize_file.is_some()
            || self.tools_inline.is_some()
            || self.tools_file.is_some()
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ImportSource {
    Inline,
    File(PathBuf),
}

impl std::fmt::Display for ImportSource {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Inline => formatter.write_str("inline"),
            Self::File(path) => write!(formatter, "{}", path.display()),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LoadedJson {
    pub json: String,
    pub source: ImportSource,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LoadedFallbackJson {
    pub initialize: LoadedJson,
    pub tools: LoadedJson,
}

#[derive(Debug, Error)]
pub enum LoadError {
    #[error("fallback import must include both initialize and tools metadata")]
    IncompletePair,
    #[error("both inline and file sources were provided for {kind}")]
    ConflictingSources { kind: &'static str },
    #[error("failed to read {kind} fallback file {path}: {source}")]
    ReadFile {
        kind: &'static str,
        path: PathBuf,
        #[source]
        source: std::io::Error,
    },
    #[error("invalid JSON syntax for {kind} fallback from {source_name}: {source}")]
    InvalidJsonSyntax {
        kind: &'static str,
        source_name: String,
        #[source]
        source: serde_json::Error,
    },
}

fn load_one(
    kind: &'static str,
    inline: &Option<String>,
    file: &Option<PathBuf>,
) -> Result<Option<LoadedJson>, LoadError> {
    let loaded = match (inline, file) {
        (Some(_), Some(_)) => return Err(LoadError::ConflictingSources { kind }),
        (None, None) => return Ok(None),
        (Some(json), None) => LoadedJson {
            json: json.clone(),
            source: ImportSource::Inline,
        },
        (None, Some(path)) => {
            let json = fs::read_to_string(path).map_err(|source| LoadError::ReadFile {
                kind,
                path: path.clone(),
                source,
            })?;
            LoadedJson {
                json,
                source: ImportSource::File(path.clone()),
            }
        }
    };

    serde_json::from_str::<Value>(&loaded.json).map_err(|source| {
        let source_name = loaded.source.to_string();
        LoadError::InvalidJsonSyntax {
            kind,
            source_name,
            source,
        }
    })?;

    Ok(Some(loaded))
}

pub fn try_load_fallback(
    spec: &RawFallbackImportSpec,
) -> Result<Option<LoadedFallbackJson>, LoadError> {
    if !spec.has_any() {
        return Ok(None);
    }

    let initialize = load_one("initialize", &spec.initialize_inline, &spec.initialize_file)?;
    let tools = load_one("tools", &spec.tools_inline, &spec.tools_file)?;

    match (initialize, tools) {
        (Some(initialize), Some(tools)) => Ok(Some(LoadedFallbackJson { initialize, tools })),
        _ => Err(LoadError::IncompletePair),
    }
}