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}