use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::{StringOrBool, deserialize_string_or_bool_opt};
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct McpConfig {
pub name: Option<String>,
pub title: Option<String>,
pub description: Option<String>,
pub homepage: Option<String>,
pub packages: Vec<McpPackage>,
pub transports: Vec<McpTransport>,
#[serde(
default,
alias = "disable",
deserialize_with = "deserialize_string_or_bool_opt"
)]
pub skip: Option<StringOrBool>,
pub repository: McpRepository,
pub auth: McpAuth,
pub registry: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct McpRepository {
pub url: String,
pub source: String,
pub id: String,
pub subfolder: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct McpAuth {
#[serde(rename = "type", default)]
pub method: McpAuthMethod,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub token: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
pub enum McpAuthMethod {
#[default]
#[serde(rename = "none")]
None,
#[serde(rename = "github")]
Github,
#[serde(rename = "github-oidc")]
GithubOidc,
}
impl McpAuthMethod {
pub fn parse(s: &str) -> anyhow::Result<Self> {
match s.trim() {
"" | "none" => Ok(Self::None),
"github" => Ok(Self::Github),
"github-oidc" => Ok(Self::GithubOidc),
other => anyhow::bail!(
"mcp: unknown auth method '{}' (expected one of: none, github, github-oidc)",
other
),
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::None => "none",
Self::Github => "github",
Self::GithubOidc => "github-oidc",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct McpPackage {
pub registry_type: McpRegistryType,
pub identifier: String,
pub transport: McpTransport,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
pub enum McpRegistryType {
#[serde(rename = "oci")]
Oci,
#[default]
#[serde(rename = "npm")]
Npm,
#[serde(rename = "pypi")]
Pypi,
#[serde(rename = "nuget")]
Nuget,
#[serde(rename = "mcpb")]
Mcpb,
}
impl McpRegistryType {
pub fn as_str(&self) -> &'static str {
match self {
Self::Oci => "oci",
Self::Npm => "npm",
Self::Pypi => "pypi",
Self::Nuget => "nuget",
Self::Mcpb => "mcpb",
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct McpTransport {
#[serde(rename = "type", default)]
pub kind: McpTransportType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
pub enum McpTransportType {
#[default]
#[serde(rename = "stdio")]
Stdio,
#[serde(rename = "streamable-http")]
StreamableHttp,
#[serde(rename = "sse")]
Sse,
}
impl McpTransportType {
pub fn as_str(&self) -> &'static str {
match self {
Self::Stdio => "stdio",
Self::StreamableHttp => "streamable-http",
Self::Sse => "sse",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn auth_method_default_is_none() {
assert_eq!(McpAuthMethod::default(), McpAuthMethod::None);
let auth = McpAuth::default();
assert_eq!(auth.method, McpAuthMethod::None);
}
#[test]
fn auth_method_parse_accepts_empty_as_none() {
assert_eq!(McpAuthMethod::parse("").unwrap(), McpAuthMethod::None);
assert_eq!(McpAuthMethod::parse("none").unwrap(), McpAuthMethod::None);
assert_eq!(
McpAuthMethod::parse("github").unwrap(),
McpAuthMethod::Github
);
assert_eq!(
McpAuthMethod::parse("github-oidc").unwrap(),
McpAuthMethod::GithubOidc
);
}
#[test]
fn auth_method_parse_rejects_unknown() {
let err = McpAuthMethod::parse("oauth").unwrap_err();
assert!(err.to_string().contains("unknown auth method"));
}
#[test]
fn yaml_roundtrip_minimal() {
let yaml = r#"
name: io.github.test/server
title: Test
description: A test server
packages:
- registry_type: oci
identifier: ghcr.io/test/server:v1.0.0
transport:
type: stdio
auth:
type: github-oidc
"#;
let cfg: McpConfig = serde_yaml_ng::from_str(yaml).expect("parse mcp yaml");
assert_eq!(cfg.name.as_deref(), Some("io.github.test/server"));
assert_eq!(cfg.packages.len(), 1);
assert_eq!(cfg.packages[0].registry_type, McpRegistryType::Oci);
assert_eq!(cfg.packages[0].transport.kind, McpTransportType::Stdio);
assert_eq!(cfg.auth.method, McpAuthMethod::GithubOidc);
}
#[test]
fn yaml_roundtrip_skip_template() {
let yaml = r#"
name: io.github.test/server
title: Test
description: A test server
skip: "{{ if .IsSnapshot }}true{{ endif }}"
"#;
let cfg: McpConfig = serde_yaml_ng::from_str(yaml).expect("parse mcp yaml");
assert!(cfg.skip.is_some());
let s = cfg.skip.as_ref().unwrap();
match s {
StringOrBool::String(v) => assert!(v.contains("IsSnapshot")),
_ => panic!("expected String variant"),
}
}
#[test]
fn yaml_roundtrip_disable_alias_for_back_compat() {
let yaml = r#"
name: io.github.test/server
disable: "{{ if .IsSnapshot }}true{{ endif }}"
"#;
let cfg: McpConfig = serde_yaml_ng::from_str(yaml).expect("parse mcp yaml");
assert!(cfg.skip.is_some(), "disable: alias must populate skip");
}
#[test]
fn auth_token_optional_and_omitted_when_empty() {
let auth = McpAuth::default();
let s = serde_yaml_ng::to_string(&auth).expect("serialize");
assert!(s.contains("type: none"), "type field always rendered");
assert!(!s.contains("token:"), "empty token omitted from yaml");
}
}