mcpkit_core/extension/
mod.rs

1//! Protocol extension infrastructure.
2//!
3//! MCP extensions provide a way to add specialized capabilities that operate
4//! outside the core protocol specification. Extensions follow these principles:
5//!
6//! - **Optional** - Implementations can choose to adopt extensions
7//! - **Additive** - Extensions add capabilities without modifying core behavior
8//! - **Composable** - Multiple extensions can work together without conflicts
9//! - **Versioned** - Extensions can be versioned independently
10//!
11//! # Usage
12//!
13//! Extensions are declared in the `experimental` field of capabilities during
14//! initialization. Each extension is identified by a unique name (typically
15//! using reverse-domain notation) and can include version and configuration.
16//!
17//! # Example
18//!
19//! ```rust
20//! use mcpkit_core::extension::{Extension, ExtensionRegistry};
21//! use mcpkit_core::capability::ServerCapabilities;
22//!
23//! // Define an extension
24//! let healthcare = Extension::new("io.health.fhir")
25//!     .with_version("1.0.0")
26//!     .with_config(serde_json::json!({
27//!         "fhir_version": "R4",
28//!         "resources": ["Patient", "Observation"]
29//!     }));
30//!
31//! // Create a registry with multiple extensions
32//! let registry = ExtensionRegistry::new()
33//!     .register(healthcare)
34//!     .register(Extension::new("io.mcp.apps").with_version("0.1.0"));
35//!
36//! // Apply to capabilities
37//! let caps = ServerCapabilities::new()
38//!     .with_tools()
39//!     .with_extensions(registry);
40//!
41//! // Check for extension support
42//! assert!(caps.has_extension("io.health.fhir"));
43//! ```
44//!
45//! # Standard Extensions
46//!
47//! The following extension namespaces are reserved:
48//!
49//! | Namespace | Description |
50//! |-----------|-------------|
51//! | `io.mcp.*` | Official MCP extensions (e.g., `io.mcp.apps`) |
52//! | `io.anthropic.*` | Anthropic-specific extensions |
53//! | `io.openai.*` | OpenAI-specific extensions |
54//!
55//! Third-party extensions should use reverse-domain notation (e.g., `com.example.myext`).
56//!
57//! # References
58//!
59//! - [MCP Extensions](https://modelcontextprotocol.io/specification/2025-11-25/extensions)
60//! - [SEP-1865: MCP Apps Extension](https://github.com/modelcontextprotocol/ext-apps)
61//!
62//! # Submodules
63//!
64//! - [`apps`] - MCP Apps extension for interactive UIs (SEP-1865)
65//! - [`discovery`] - Extension discovery and negotiation utilities
66//! - [`templates`] - Domain-specific extension templates (healthcare, finance, `IoT`)
67
68pub mod apps;
69pub mod discovery;
70pub mod templates;
71
72use serde::{Deserialize, Serialize};
73use std::collections::HashMap;
74
75/// A protocol extension declaration.
76///
77/// Extensions are identified by a unique name (typically reverse-domain notation)
78/// and can include version information and custom configuration.
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct Extension {
81    /// Extension name (e.g., "io.mcp.apps", "com.example.myext").
82    pub name: String,
83
84    /// Extension version (semver recommended).
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub version: Option<String>,
87
88    /// Extension-specific configuration.
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub config: Option<serde_json::Value>,
91
92    /// Human-readable description.
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub description: Option<String>,
95}
96
97impl Extension {
98    /// Create a new extension with the given name.
99    ///
100    /// # Arguments
101    ///
102    /// * `name` - Unique extension identifier (reverse-domain notation recommended)
103    ///
104    /// # Example
105    ///
106    /// ```rust
107    /// use mcpkit_core::extension::Extension;
108    ///
109    /// let ext = Extension::new("com.example.myext");
110    /// assert_eq!(ext.name, "com.example.myext");
111    /// ```
112    #[must_use]
113    pub fn new(name: impl Into<String>) -> Self {
114        Self {
115            name: name.into(),
116            version: None,
117            config: None,
118            description: None,
119        }
120    }
121
122    /// Set the extension version.
123    ///
124    /// # Arguments
125    ///
126    /// * `version` - Semver version string (e.g., "1.0.0")
127    #[must_use]
128    pub fn with_version(mut self, version: impl Into<String>) -> Self {
129        self.version = Some(version.into());
130        self
131    }
132
133    /// Set extension-specific configuration.
134    ///
135    /// # Arguments
136    ///
137    /// * `config` - JSON configuration object
138    #[must_use]
139    pub fn with_config(mut self, config: serde_json::Value) -> Self {
140        self.config = Some(config);
141        self
142    }
143
144    /// Set a human-readable description.
145    ///
146    /// # Arguments
147    ///
148    /// * `description` - Extension description
149    #[must_use]
150    pub fn with_description(mut self, description: impl Into<String>) -> Self {
151        self.description = Some(description.into());
152        self
153    }
154}
155
156/// Registry of protocol extensions.
157///
158/// The registry provides a structured way to declare and manage multiple
159/// extensions. It serializes to a format compatible with the `experimental`
160/// capabilities field.
161#[derive(Debug, Clone, Default, Serialize, Deserialize)]
162pub struct ExtensionRegistry {
163    /// Registered extensions indexed by name.
164    extensions: HashMap<String, Extension>,
165}
166
167impl ExtensionRegistry {
168    /// Create a new empty extension registry.
169    #[must_use]
170    pub fn new() -> Self {
171        Self::default()
172    }
173
174    /// Register an extension.
175    ///
176    /// If an extension with the same name already exists, it will be replaced.
177    ///
178    /// # Arguments
179    ///
180    /// * `extension` - The extension to register
181    #[must_use]
182    pub fn register(mut self, extension: Extension) -> Self {
183        self.extensions.insert(extension.name.clone(), extension);
184        self
185    }
186
187    /// Check if an extension is registered.
188    ///
189    /// # Arguments
190    ///
191    /// * `name` - Extension name to check
192    #[must_use]
193    pub fn has(&self, name: &str) -> bool {
194        self.extensions.contains_key(name)
195    }
196
197    /// Get an extension by name.
198    ///
199    /// # Arguments
200    ///
201    /// * `name` - Extension name to retrieve
202    #[must_use]
203    pub fn get(&self, name: &str) -> Option<&Extension> {
204        self.extensions.get(name)
205    }
206
207    /// Get all registered extension names.
208    #[must_use]
209    pub fn names(&self) -> impl Iterator<Item = &str> {
210        self.extensions.keys().map(String::as_str)
211    }
212
213    /// Get the number of registered extensions.
214    #[must_use]
215    pub fn len(&self) -> usize {
216        self.extensions.len()
217    }
218
219    /// Check if the registry is empty.
220    #[must_use]
221    pub fn is_empty(&self) -> bool {
222        self.extensions.is_empty()
223    }
224
225    /// Convert the registry to a JSON value for the `experimental` field.
226    ///
227    /// The output format is:
228    /// ```json
229    /// {
230    ///   "extensions": {
231    ///     "io.mcp.apps": { "version": "0.1.0", ... },
232    ///     "com.example.myext": { "version": "1.0.0", ... }
233    ///   }
234    /// }
235    /// ```
236    #[must_use]
237    pub fn to_experimental(&self) -> serde_json::Value {
238        serde_json::json!({
239            "extensions": self.extensions
240        })
241    }
242
243    /// Parse an extension registry from an `experimental` field value.
244    ///
245    /// # Arguments
246    ///
247    /// * `experimental` - The experimental field value from capabilities
248    ///
249    /// # Returns
250    ///
251    /// An extension registry, or `None` if parsing fails.
252    #[must_use]
253    pub fn from_experimental(experimental: &serde_json::Value) -> Option<Self> {
254        let extensions = experimental.get("extensions")?.as_object()?;
255        let mut registry = Self::new();
256
257        for (name, value) in extensions {
258            if let Ok(mut ext) = serde_json::from_value::<Extension>(value.clone()) {
259                ext.name.clone_from(name);
260                registry = registry.register(ext);
261            }
262        }
263
264        Some(registry)
265    }
266}
267
268/// Known extension namespaces.
269pub mod namespaces {
270    /// Official MCP extensions namespace.
271    pub const MCP: &str = "io.mcp";
272
273    /// MCP Apps extension (SEP-1865).
274    pub const MCP_APPS: &str = "io.mcp.apps";
275
276    /// Anthropic-specific extensions.
277    pub const ANTHROPIC: &str = "io.anthropic";
278
279    /// OpenAI-specific extensions.
280    pub const OPENAI: &str = "io.openai";
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    #[test]
288    fn test_extension_builder() {
289        let ext = Extension::new("com.example.test")
290            .with_version("1.0.0")
291            .with_description("Test extension")
292            .with_config(serde_json::json!({"enabled": true}));
293
294        assert_eq!(ext.name, "com.example.test");
295        assert_eq!(ext.version, Some("1.0.0".to_string()));
296        assert_eq!(ext.description, Some("Test extension".to_string()));
297        assert!(ext.config.is_some());
298    }
299
300    #[test]
301    fn test_extension_registry() {
302        let registry = ExtensionRegistry::new()
303            .register(Extension::new("io.mcp.apps").with_version("0.1.0"))
304            .register(Extension::new("com.example.ext").with_version("2.0.0"));
305
306        assert!(registry.has("io.mcp.apps"));
307        assert!(registry.has("com.example.ext"));
308        assert!(!registry.has("unknown"));
309        assert_eq!(registry.len(), 2);
310    }
311
312    #[test]
313    fn test_experimental_roundtrip() {
314        let registry =
315            ExtensionRegistry::new().register(Extension::new("io.mcp.apps").with_version("0.1.0"));
316
317        let json = registry.to_experimental();
318        let parsed = ExtensionRegistry::from_experimental(&json).unwrap();
319
320        assert!(parsed.has("io.mcp.apps"));
321        assert_eq!(
322            parsed.get("io.mcp.apps").unwrap().version,
323            Some("0.1.0".to_string())
324        );
325    }
326
327    #[test]
328    fn test_serialization() {
329        let ext = Extension::new("test").with_version("1.0.0");
330        let json = serde_json::to_string(&ext).unwrap();
331        assert!(json.contains("\"name\":\"test\""));
332        assert!(json.contains("\"version\":\"1.0.0\""));
333    }
334}