pub mod apps;
pub mod discovery;
pub mod templates;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Extension {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub config: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
impl Extension {
#[must_use]
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
version: None,
config: None,
description: None,
}
}
#[must_use]
pub fn with_version(mut self, version: impl Into<String>) -> Self {
self.version = Some(version.into());
self
}
#[must_use]
pub fn with_config(mut self, config: serde_json::Value) -> Self {
self.config = Some(config);
self
}
#[must_use]
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ExtensionRegistry {
extensions: HashMap<String, Extension>,
}
impl ExtensionRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn register(mut self, extension: Extension) -> Self {
self.extensions.insert(extension.name.clone(), extension);
self
}
#[must_use]
pub fn has(&self, name: &str) -> bool {
self.extensions.contains_key(name)
}
#[must_use]
pub fn get(&self, name: &str) -> Option<&Extension> {
self.extensions.get(name)
}
#[must_use]
pub fn names(&self) -> impl Iterator<Item = &str> {
self.extensions.keys().map(String::as_str)
}
#[must_use]
pub fn len(&self) -> usize {
self.extensions.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.extensions.is_empty()
}
#[must_use]
pub fn to_experimental(&self) -> serde_json::Value {
serde_json::json!({
"extensions": self.extensions
})
}
#[must_use]
pub fn from_experimental(experimental: &serde_json::Value) -> Option<Self> {
let extensions = experimental.get("extensions")?.as_object()?;
let mut registry = Self::new();
for (name, value) in extensions {
if let Ok(mut ext) = serde_json::from_value::<Extension>(value.clone()) {
ext.name.clone_from(name);
registry = registry.register(ext);
}
}
Some(registry)
}
}
pub mod namespaces {
pub const MCP: &str = "io.mcp";
pub const MCP_APPS: &str = "io.mcp.apps";
pub const ANTHROPIC: &str = "io.anthropic";
pub const OPENAI: &str = "io.openai";
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extension_builder() {
let ext = Extension::new("com.example.test")
.with_version("1.0.0")
.with_description("Test extension")
.with_config(serde_json::json!({"enabled": true}));
assert_eq!(ext.name, "com.example.test");
assert_eq!(ext.version, Some("1.0.0".to_string()));
assert_eq!(ext.description, Some("Test extension".to_string()));
assert!(ext.config.is_some());
}
#[test]
fn test_extension_registry() {
let registry = ExtensionRegistry::new()
.register(Extension::new("io.mcp.apps").with_version("0.1.0"))
.register(Extension::new("com.example.ext").with_version("2.0.0"));
assert!(registry.has("io.mcp.apps"));
assert!(registry.has("com.example.ext"));
assert!(!registry.has("unknown"));
assert_eq!(registry.len(), 2);
}
#[test]
fn test_experimental_roundtrip() {
let registry =
ExtensionRegistry::new().register(Extension::new("io.mcp.apps").with_version("0.1.0"));
let json = registry.to_experimental();
let parsed = ExtensionRegistry::from_experimental(&json).unwrap();
assert!(parsed.has("io.mcp.apps"));
assert_eq!(
parsed.get("io.mcp.apps").unwrap().version,
Some("0.1.0".to_string())
);
}
#[test]
fn test_serialization() {
let ext = Extension::new("test").with_version("1.0.0");
let json = serde_json::to_string(&ext).unwrap();
assert!(json.contains("\"name\":\"test\""));
assert!(json.contains("\"version\":\"1.0.0\""));
}
}