diff --git a/crates/rmcp/src/model/capabilities.rs b/crates/rmcp/src/model/capabilities.rs index 80353216..c8255751 100644 --- a/crates/rmcp/src/model/capabilities.rs +++ b/crates/rmcp/src/model/capabilities.rs @@ -6,6 +6,28 @@ use serde::{Deserialize, Serialize}; use super::JsonObject; pub type ExperimentalCapabilities = BTreeMap; +/// MCP extension capabilities map. +/// +/// Keys are extension identifiers in the format `{vendor-prefix}/{extension-name}` +/// (e.g., `io.modelcontextprotocol/ui`, `io.modelcontextprotocol/oauth-client-credentials`). +/// Values are per-extension settings objects. An empty object indicates support with no settings. +/// +/// # Example +/// +/// ```rust +/// use rmcp::model::ExtensionCapabilities; +/// use serde_json::json; +/// +/// let mut extensions = ExtensionCapabilities::new(); +/// extensions.insert( +/// "io.modelcontextprotocol/ui".to_string(), +/// serde_json::from_value(json!({ +/// "mimeTypes": ["text/html;profile=mcp-app"] +/// })).unwrap() +/// ); +/// ``` +pub type ExtensionCapabilities = BTreeMap; + #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] @@ -187,6 +209,12 @@ pub struct ElicitationCapability { pub struct ClientCapabilities { #[serde(skip_serializing_if = "Option::is_none")] pub experimental: Option, + /// Optional MCP extensions that the client supports (SEP-1724). + /// Keys are extension identifiers (e.g., `"io.modelcontextprotocol/ui"`), + /// values are per-extension settings objects. An empty object indicates + /// support with no settings. + #[serde(skip_serializing_if = "Option::is_none")] + pub extensions: Option, #[serde(skip_serializing_if = "Option::is_none")] pub roots: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -217,6 +245,12 @@ pub struct ClientCapabilities { pub struct ServerCapabilities { #[serde(skip_serializing_if = "Option::is_none")] pub experimental: Option, + /// Optional MCP extensions that the server supports (SEP-1724). + /// Keys are extension identifiers (e.g., `"io.modelcontextprotocol/apps"`), + /// values are per-extension settings objects. An empty object indicates + /// support with no settings. + #[serde(skip_serializing_if = "Option::is_none")] + pub extensions: Option, #[serde(skip_serializing_if = "Option::is_none")] pub logging: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -339,6 +373,7 @@ macro_rules! builder { builder! { ServerCapabilities { experimental: ExperimentalCapabilities, + extensions: ExtensionCapabilities, logging: JsonObject, completions: JsonObject, prompts: PromptsCapability, @@ -348,8 +383,15 @@ builder! { } } -impl - ServerCapabilitiesBuilder> +impl< + const E: bool, + const EXT: bool, + const L: bool, + const C: bool, + const P: bool, + const R: bool, + const TASKS: bool, +> ServerCapabilitiesBuilder> { pub fn enable_tool_list_changed(mut self) -> Self { if let Some(c) = self.tools.as_mut() { @@ -359,8 +401,15 @@ impl - ServerCapabilitiesBuilder> +impl< + const E: bool, + const EXT: bool, + const L: bool, + const C: bool, + const R: bool, + const T: bool, + const TASKS: bool, +> ServerCapabilitiesBuilder> { pub fn enable_prompts_list_changed(mut self) -> Self { if let Some(c) = self.prompts.as_mut() { @@ -370,8 +419,15 @@ impl - ServerCapabilitiesBuilder> +impl< + const E: bool, + const EXT: bool, + const L: bool, + const C: bool, + const P: bool, + const T: bool, + const TASKS: bool, +> ServerCapabilitiesBuilder> { pub fn enable_resources_list_changed(mut self) -> Self { if let Some(c) = self.resources.as_mut() { @@ -391,6 +447,7 @@ impl - ClientCapabilitiesBuilder> +impl + ClientCapabilitiesBuilder> { pub fn enable_roots_list_changed(mut self) -> Self { if let Some(c) = self.roots.as_mut() { @@ -410,8 +467,8 @@ impl } #[cfg(feature = "elicitation")] -impl - ClientCapabilitiesBuilder> +impl + ClientCapabilitiesBuilder> { /// Enable JSON Schema validation for elicitation responses. /// When enabled, the client will validate user input against the requested_schema @@ -528,4 +585,90 @@ mod test { assert_eq!(json["cancel"], serde_json::json!({})); assert_eq!(json["requests"]["tools"]["call"], serde_json::json!({})); } + + #[test] + fn test_client_extensions_capability() { + // Test building ClientCapabilities with extensions (MCP Apps support) + let mut extensions = ExtensionCapabilities::new(); + extensions.insert( + "io.modelcontextprotocol/ui".to_string(), + serde_json::from_value(serde_json::json!({ + "mimeTypes": ["text/html;profile=mcp-app"] + })) + .unwrap(), + ); + + let capabilities = ClientCapabilities::builder() + .enable_extensions_with(extensions) + .enable_sampling() + .build(); + + // Verify serialization matches MCP Apps spec format + let json = serde_json::to_value(&capabilities).unwrap(); + assert_eq!( + json["extensions"]["io.modelcontextprotocol/ui"]["mimeTypes"], + serde_json::json!(["text/html;profile=mcp-app"]) + ); + assert!(json["sampling"].is_object()); + } + + #[test] + fn test_server_extensions_capability() { + // Test building ServerCapabilities with extensions + let mut extensions = ExtensionCapabilities::new(); + extensions.insert( + "io.modelcontextprotocol/apps".to_string(), + serde_json::from_value(serde_json::json!({})).unwrap(), + ); + + let capabilities = ServerCapabilities::builder() + .enable_extensions_with(extensions) + .enable_tools() + .build(); + + // Verify serialization + let json = serde_json::to_value(&capabilities).unwrap(); + assert!(json["extensions"]["io.modelcontextprotocol/apps"].is_object()); + assert!(json["tools"].is_object()); + } + + #[test] + fn test_extensions_deserialization() { + // Test deserializing capabilities with extensions from JSON + let json = serde_json::json!({ + "extensions": { + "io.modelcontextprotocol/ui": { + "mimeTypes": ["text/html;profile=mcp-app"] + } + }, + "sampling": {} + }); + + let capabilities: ClientCapabilities = serde_json::from_value(json).unwrap(); + assert!(capabilities.extensions.is_some()); + let extensions = capabilities.extensions.unwrap(); + assert!(extensions.contains_key("io.modelcontextprotocol/ui")); + let ui_ext = extensions.get("io.modelcontextprotocol/ui").unwrap(); + assert!(ui_ext.contains_key("mimeTypes")); + } + + #[test] + fn test_extensions_empty_settings() { + // Test that empty extension settings work (indicates support with no settings) + let mut extensions = ExtensionCapabilities::new(); + extensions.insert( + "io.modelcontextprotocol/oauth-client-credentials".to_string(), + JsonObject::new(), + ); + + let capabilities = ClientCapabilities::builder() + .enable_extensions_with(extensions) + .build(); + + let json = serde_json::to_value(&capabilities).unwrap(); + assert_eq!( + json["extensions"]["io.modelcontextprotocol/oauth-client-credentials"], + serde_json::json!({}) + ); + } } diff --git a/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json b/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json index 6e45c5f1..4b6c24aa 100644 --- a/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json +++ b/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json @@ -296,6 +296,17 @@ "additionalProperties": true } }, + "extensions": { + "description": "Optional MCP extensions that the client supports (SEP-1724).\nKeys are extension identifiers (e.g., `\"io.modelcontextprotocol/ui\"`),\nvalues are per-extension settings objects. An empty object indicates\nsupport with no settings.", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "object", + "additionalProperties": true + } + }, "roots": { "anyOf": [ { diff --git a/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema_current.json b/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema_current.json index 6e45c5f1..4b6c24aa 100644 --- a/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema_current.json +++ b/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema_current.json @@ -296,6 +296,17 @@ "additionalProperties": true } }, + "extensions": { + "description": "Optional MCP extensions that the client supports (SEP-1724).\nKeys are extension identifiers (e.g., `\"io.modelcontextprotocol/ui\"`),\nvalues are per-extension settings objects. An empty object indicates\nsupport with no settings.", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "object", + "additionalProperties": true + } + }, "roots": { "anyOf": [ { diff --git a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json index f1596857..e23aa330 100644 --- a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json +++ b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json @@ -2369,6 +2369,17 @@ "additionalProperties": true } }, + "extensions": { + "description": "Optional MCP extensions that the server supports (SEP-1724).\nKeys are extension identifiers (e.g., `\"io.modelcontextprotocol/apps\"`),\nvalues are per-extension settings objects. An empty object indicates\nsupport with no settings.", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "object", + "additionalProperties": true + } + }, "logging": { "type": [ "object", diff --git a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json index f1596857..e23aa330 100644 --- a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json +++ b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json @@ -2369,6 +2369,17 @@ "additionalProperties": true } }, + "extensions": { + "description": "Optional MCP extensions that the server supports (SEP-1724).\nKeys are extension identifiers (e.g., `\"io.modelcontextprotocol/apps\"`),\nvalues are per-extension settings objects. An empty object indicates\nsupport with no settings.", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "object", + "additionalProperties": true + } + }, "logging": { "type": [ "object",