use std::collections::BTreeMap; #[cfg(any(feature = "server", feature = "macros"))] use std::marker::PhantomData; #[cfg(any(feature = "server", feature = "macros"))] use pastey::paste; 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))] #[non_exhaustive] pub struct PromptsCapability { #[serde(skip_serializing_if = "Option::is_none")] pub list_changed: Option, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[non_exhaustive] pub struct ResourcesCapability { #[serde(skip_serializing_if = "Option::is_none")] pub subscribe: Option, #[serde(skip_serializing_if = "Option::is_none")] pub list_changed: Option, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[non_exhaustive] pub struct ToolsCapability { #[serde(skip_serializing_if = "Option::is_none")] pub list_changed: Option, } /// Roots capability. Deprecated by SEP-2577; remains functional and will be /// removed in a future release. /// See . #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[non_exhaustive] pub struct RootsCapabilities { #[serde(skip_serializing_if = "Option::is_none")] pub list_changed: Option, } /// Capability for handling elicitation requests from servers. /// Elicitation allows servers to request interactive input from users during tool execution. /// This capability indicates that a client can handle elicitation requests and present /// appropriate UI to users for collecting the requested information. /// /// Capability for form mode elicitation. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[non_exhaustive] pub struct FormElicitationCapability { /// Whether the client supports JSON Schema validation for elicitation responses. /// When true, the client will validate user input against the requested_schema /// before sending the response back to the server. #[serde(skip_serializing_if = "Option::is_none")] pub schema_validation: Option, } impl FormElicitationCapability { pub fn new() -> Self { Self::default() } pub fn with_schema_validation(mut self, enabled: bool) -> Self { self.schema_validation = Some(enabled); self } } /// Capability for URL mode elicitation. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[non_exhaustive] pub struct UrlElicitationCapability {} impl UrlElicitationCapability { pub fn new() -> Self { Self::default() } } /// Elicitation allows servers to request interactive input from users during tool execution. /// This capability indicates that a client can handle elicitation requests and present /// appropriate UI to users for collecting the requested information. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[non_exhaustive] pub struct ElicitationCapability { /// Whether client supports form-based elicitation. #[serde(skip_serializing_if = "Option::is_none")] pub form: Option, /// Whether client supports URL-based elicitation. #[serde(skip_serializing_if = "Option::is_none")] pub url: Option, } impl ElicitationCapability { pub fn new() -> Self { Self::default() } pub fn with_form(mut self, form: FormElicitationCapability) -> Self { self.form = Some(form); self } pub fn with_url(mut self, url: UrlElicitationCapability) -> Self { self.url = Some(url); self } } /// Sampling capability with optional sub-capabilities (SEP-1577). /// /// Deprecated by SEP-2577; remains functional and will be removed in a future /// release. /// See . #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[non_exhaustive] pub struct SamplingCapability { /// Support for `tools` and `toolChoice` parameters #[serde(skip_serializing_if = "Option::is_none")] pub tools: Option, /// Support for `includeContext` (soft-deprecated) #[serde(skip_serializing_if = "Option::is_none")] pub context: Option, } /// /// # Builder /// ```rust /// # use rmcp::model::ClientCapabilities; /// let cap = ClientCapabilities::builder() /// .enable_experimental() /// .build(); /// ``` #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[non_exhaustive] 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, /// Capability for filesystem roots (deprecated by SEP-2577). #[serde(skip_serializing_if = "Option::is_none")] pub roots: Option, /// Capability for LLM sampling requests (SEP-1577, deprecated by SEP-2577). #[serde(skip_serializing_if = "Option::is_none")] pub sampling: Option, /// Capability to handle elicitation requests from servers for interactive user input #[serde(skip_serializing_if = "Option::is_none")] pub elicitation: Option, } impl ClientCapabilities { /// Returns `true` if the `io.modelcontextprotocol/tasks` extension /// (SEP-2663) is declared in [`Self::extensions`]. pub fn supports_tasks(&self) -> bool { self.extensions .as_ref() .is_some_and(|e| e.contains_key(super::TASKS_EXTENSION_ID)) } } /// /// ## Builder /// ```rust /// # use rmcp::model::ServerCapabilities; /// let cap = ServerCapabilities::builder() /// .enable_experimental() /// .enable_prompts() /// .enable_resources() /// .enable_tools() /// .enable_tool_list_changed() /// .build(); /// ``` #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[non_exhaustive] 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, /// Capability for server log message notifications (deprecated by SEP-2577). #[serde(skip_serializing_if = "Option::is_none")] pub logging: Option, #[serde(skip_serializing_if = "Option::is_none")] pub completions: Option, #[serde(skip_serializing_if = "Option::is_none")] pub prompts: Option, #[serde(skip_serializing_if = "Option::is_none")] pub resources: Option, #[serde(skip_serializing_if = "Option::is_none")] pub tools: Option, } impl ServerCapabilities { /// Returns `true` if the `io.modelcontextprotocol/tasks` extension /// (SEP-2663) is declared in [`Self::extensions`]. pub fn supports_tasks(&self) -> bool { self.extensions .as_ref() .is_some_and(|e| e.contains_key(super::TASKS_EXTENSION_ID)) } } #[cfg(any(feature = "server", feature = "macros"))] macro_rules! builder { ($Target: ident {$($(#[$fa:meta])* $f: ident: $T: ty),* $(,)?}) => { paste! { #[derive(Default, Clone, Copy, Debug)] #[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct [<$Target BuilderState>]< $(const [<$f:upper>]: bool = false,)* >; #[derive(Debug, Default)] #[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct [<$Target Builder>]]> { $(pub $f: Option<$T>,)* pub state: PhantomData } impl $Target { #[doc = "Create a new [`" $Target "`] builder."] pub fn builder() -> [<$Target Builder>] { <[<$Target Builder>]>::default() } } impl [<$Target Builder>] { pub fn build(self) -> $Target { $Target { $( $f: self.$f, )* } } } impl From<[<$Target Builder>]> for $Target { fn from(builder: [<$Target Builder>]) -> Self { builder.build() } } } builder!($Target @toggle $($(#[$fa])* $f: $T,)*); }; ($Target: ident @toggle $(#[$fa0:meta])* $f0: ident: $T0: ty, $($(#[$fa:meta])* $f: ident: $T: ty,)*) => { builder!($Target @toggle [][$(#[$fa0])* $f0: $T0][$($(#[$fa])* $f: $T,)*]); }; ($Target: ident @toggle [$($ff: ident: $Tf: ty,)*][$(#[$fna:meta])* $fn: ident: $TN: ty][$(#[$fn1a:meta])* $fn_1: ident: $Tn_1: ty, $($(#[$fta:meta])* $ft: ident: $Tt: ty,)*]) => { builder!($Target @impl_toggle [$($ff: $Tf,)*][$(#[$fna])* $fn: $TN][$fn_1: $Tn_1, $($ft:$Tt,)*]); builder!($Target @toggle [$($ff: $Tf,)* $fn: $TN,][$(#[$fn1a])* $fn_1: $Tn_1][$($(#[$fta])* $ft: $Tt,)*]); }; ($Target: ident @toggle [$($ff: ident: $Tf: ty,)*][$(#[$fna:meta])* $fn: ident: $TN: ty][]) => { builder!($Target @impl_toggle [$($ff: $Tf,)*][$(#[$fna])* $fn: $TN][]); }; ($Target: ident @impl_toggle [$($ff: ident: $Tf: ty,)*][$(#[$fna:meta])* $fn: ident: $TN: ty][$($ft: ident: $Tt: ty,)*]) => { paste! { impl< $(const [<$ff:upper>]: bool,)* $(const [<$ft:upper>]: bool,)* > [<$Target Builder>]<[<$Target BuilderState>]< $([<$ff:upper>],)* false, $([<$ft:upper>],)* >> { $(#[$fna])* pub fn [](self) -> [<$Target Builder>]<[<$Target BuilderState>]< $([<$ff:upper>],)* true, $([<$ft:upper>],)* >> { [<$Target Builder>] { $( $ff: self.$ff, )* $fn: Some($TN::default()), $( $ft: self.$ft, )* state: PhantomData } } $(#[$fna])* pub fn [](self, $fn: $TN) -> [<$Target Builder>]<[<$Target BuilderState>]< $([<$ff:upper>],)* true, $([<$ft:upper>],)* >> { [<$Target Builder>] { $( $ff: self.$ff, )* $fn: Some($fn), $( $ft: self.$ft, )* state: PhantomData } } } // do we really need to disable some thing in builder? // impl< // $(const [<$ff:upper>]: bool,)* // $(const [<$ft:upper>]: bool,)* // > [<$Target Builder>]<[<$Target BuilderState>]< // $([<$ff:upper>],)* // true, // $([<$ft:upper>],)* // >> { // pub fn [](self) -> [<$Target Builder>]<[<$Target BuilderState>]< // $([<$ff:upper>],)* // false, // $([<$ft:upper>],)* // >> { // [<$Target Builder>] { // $( $ff: self.$ff, )* // $fn: None, // $( $ft: self.$ft, )* // state: PhantomData // } // } // } } } } #[cfg(any(feature = "server", feature = "macros"))] builder! { ServerCapabilities { experimental: ExperimentalCapabilities, extensions: ExtensionCapabilities, #[deprecated( since = "1.8.0", note = "Logging is deprecated by SEP-2577 and will be removed in a future release. See https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577" )] logging: JsonObject, completions: JsonObject, prompts: PromptsCapability, resources: ResourcesCapability, tools: ToolsCapability, } } #[cfg(any(feature = "server", feature = "macros"))] impl ServerCapabilitiesBuilder> { pub fn enable_tool_list_changed(mut self) -> Self { if let Some(c) = self.tools.as_mut() { c.list_changed = Some(true); } self } } #[cfg(any(feature = "server", feature = "macros"))] impl ServerCapabilitiesBuilder> { pub fn enable_prompts_list_changed(mut self) -> Self { if let Some(c) = self.prompts.as_mut() { c.list_changed = Some(true); } self } } #[cfg(any(feature = "server", feature = "macros"))] impl ServerCapabilitiesBuilder> { pub fn enable_resources_list_changed(mut self) -> Self { if let Some(c) = self.resources.as_mut() { c.list_changed = Some(true); } self } pub fn enable_resources_subscribe(mut self) -> Self { if let Some(c) = self.resources.as_mut() { c.subscribe = Some(true); } self } } #[cfg(any(feature = "server", feature = "macros"))] impl ServerCapabilitiesBuilder { /// Declare support for the `io.modelcontextprotocol/tasks` extension /// (SEP-2663) in the `extensions` capability map. pub fn enable_tasks(mut self) -> Self { self.extensions .get_or_insert_with(ExtensionCapabilities::new) .insert(super::TASKS_EXTENSION_ID.to_string(), JsonObject::new()); self } } #[cfg(any(feature = "server", feature = "macros"))] builder! { ClientCapabilities{ experimental: ExperimentalCapabilities, extensions: ExtensionCapabilities, #[deprecated( since = "1.8.0", note = "Roots is deprecated by SEP-2577 and will be removed in a future release. See https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577" )] roots: RootsCapabilities, #[deprecated( since = "1.8.0", note = "Sampling is deprecated by SEP-2577 and will be removed in a future release. See https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577" )] sampling: SamplingCapability, elicitation: ElicitationCapability, } } #[cfg(any(feature = "server", feature = "macros"))] impl ClientCapabilitiesBuilder { /// Declare support for the `io.modelcontextprotocol/tasks` extension /// (SEP-2663) in the `extensions` capability map. pub fn enable_tasks(mut self) -> Self { self.extensions .get_or_insert_with(ExtensionCapabilities::new) .insert(super::TASKS_EXTENSION_ID.to_string(), JsonObject::new()); self } } #[cfg(any(feature = "server", feature = "macros"))] impl ClientCapabilitiesBuilder> { #[deprecated( since = "1.8.0", note = "Roots is deprecated by SEP-2577 and will be removed in a future release. See https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577" )] pub fn enable_roots_list_changed(mut self) -> Self { if let Some(c) = self.roots.as_mut() { c.list_changed = Some(true); } self } } #[cfg(any(feature = "server", feature = "macros"))] impl ClientCapabilitiesBuilder> { /// Enable tool calling in sampling requests #[deprecated( since = "1.8.0", note = "Sampling is deprecated by SEP-2577 and will be removed in a future release. See https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577" )] pub fn enable_sampling_tools(mut self) -> Self { if let Some(c) = self.sampling.as_mut() { c.tools = Some(JsonObject::default()); } self } /// Enable context inclusion in sampling (soft-deprecated) #[deprecated( since = "1.8.0", note = "Sampling is deprecated by SEP-2577 and will be removed in a future release. See https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577" )] pub fn enable_sampling_context(mut self) -> Self { if let Some(c) = self.sampling.as_mut() { c.context = Some(JsonObject::default()); } self } } #[cfg(all(feature = "elicitation", any(feature = "server", feature = "macros")))] impl ClientCapabilitiesBuilder> { /// Enable JSON Schema validation for elicitation responses in form mode. /// When enabled, the client will validate user input against the requested_schema /// before sending responses back to the server. pub fn enable_elicitation_schema_validation(mut self) -> Self { if let Some(c) = self.elicitation.as_mut() { c.form = Some(FormElicitationCapability { schema_validation: Some(true), }); } self } } #[cfg(test)] #[cfg(any(feature = "server", feature = "macros"))] mod test { use super::*; #[test] #[allow(deprecated)] fn test_builder() { let builder = ::default() .enable_logging() .enable_experimental() .enable_prompts() .enable_resources() .enable_tools() .enable_tool_list_changed(); assert_eq!(builder.logging, Some(JsonObject::default())); assert_eq!(builder.prompts, Some(PromptsCapability::default())); assert_eq!(builder.resources, Some(ResourcesCapability::default())); assert_eq!( builder.tools, Some(ToolsCapability { list_changed: Some(true), }) ); assert_eq!( builder.experimental, Some(ExperimentalCapabilities::default()) ); let client_builder = ::default() .enable_experimental() .enable_roots() .enable_roots_list_changed() .enable_sampling(); assert_eq!( client_builder.experimental, Some(ExperimentalCapabilities::default()) ); assert_eq!( client_builder.roots, Some(RootsCapabilities { list_changed: Some(true), }) ); } #[test] fn test_tasks_extension_capability() { // SEP-2663: tasks are declared via the extensions map. let capabilities = ClientCapabilities::builder().enable_tasks().build(); assert!(capabilities.supports_tasks()); let json = serde_json::to_value(&capabilities).unwrap(); assert_eq!( json["extensions"][crate::model::TASKS_EXTENSION_ID], serde_json::json!({}) ); let server = ServerCapabilities::builder().enable_tasks().build(); assert!(server.supports_tasks()); let json = serde_json::to_value(&server).unwrap(); assert_eq!( json["extensions"][crate::model::TASKS_EXTENSION_ID], serde_json::json!({}) ); } #[test] #[allow(deprecated)] 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!({}) ); } }