Skip to main content

rmcp/model/
capabilities.rs

1use std::collections::BTreeMap;
2#[cfg(any(feature = "server", feature = "macros"))]
3use std::marker::PhantomData;
4
5#[cfg(any(feature = "server", feature = "macros"))]
6use pastey::paste;
7use serde::{Deserialize, Serialize};
8
9use super::JsonObject;
10pub type ExperimentalCapabilities = BTreeMap<String, JsonObject>;
11
12/// MCP extension capabilities map.
13///
14/// Keys are extension identifiers in the format `{vendor-prefix}/{extension-name}`
15/// (e.g., `io.modelcontextprotocol/ui`, `io.modelcontextprotocol/oauth-client-credentials`).
16/// Values are per-extension settings objects. An empty object indicates support with no settings.
17///
18/// # Example
19///
20/// ```rust
21/// use rmcp::model::ExtensionCapabilities;
22/// use serde_json::json;
23///
24/// let mut extensions = ExtensionCapabilities::new();
25/// extensions.insert(
26///     "io.modelcontextprotocol/ui".to_string(),
27///     serde_json::from_value(json!({
28///         "mimeTypes": ["text/html;profile=mcp-app"]
29///     })).unwrap()
30/// );
31/// ```
32pub type ExtensionCapabilities = BTreeMap<String, JsonObject>;
33
34#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
35#[serde(rename_all = "camelCase")]
36#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
37#[non_exhaustive]
38pub struct PromptsCapability {
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub list_changed: Option<bool>,
41}
42
43#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
44#[serde(rename_all = "camelCase")]
45#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
46#[non_exhaustive]
47pub struct ResourcesCapability {
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub subscribe: Option<bool>,
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub list_changed: Option<bool>,
52}
53
54#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
55#[serde(rename_all = "camelCase")]
56#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
57#[non_exhaustive]
58pub struct ToolsCapability {
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub list_changed: Option<bool>,
61}
62
63/// Roots capability. Deprecated by SEP-2577; remains functional and will be
64/// removed in a future release.
65/// See <https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577>.
66#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
67#[serde(rename_all = "camelCase")]
68#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
69#[non_exhaustive]
70pub struct RootsCapabilities {
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub list_changed: Option<bool>,
73}
74
75/// Capability for handling elicitation requests from servers.
76/// Elicitation allows servers to request interactive input from users during tool execution.
77/// This capability indicates that a client can handle elicitation requests and present
78/// appropriate UI to users for collecting the requested information.
79///
80/// Capability for form mode elicitation.
81#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
82#[serde(rename_all = "camelCase")]
83#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
84#[non_exhaustive]
85pub struct FormElicitationCapability {
86    /// Whether the client supports JSON Schema validation for elicitation responses.
87    /// When true, the client will validate user input against the requested_schema
88    /// before sending the response back to the server.
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub schema_validation: Option<bool>,
91}
92
93impl FormElicitationCapability {
94    pub fn new() -> Self {
95        Self::default()
96    }
97
98    pub fn with_schema_validation(mut self, enabled: bool) -> Self {
99        self.schema_validation = Some(enabled);
100        self
101    }
102}
103
104/// Capability for URL mode elicitation.
105#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
106#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
107#[non_exhaustive]
108pub struct UrlElicitationCapability {}
109
110impl UrlElicitationCapability {
111    pub fn new() -> Self {
112        Self::default()
113    }
114}
115
116/// Elicitation allows servers to request interactive input from users during tool execution.
117/// This capability indicates that a client can handle elicitation requests and present
118/// appropriate UI to users for collecting the requested information.
119#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
120#[serde(rename_all = "camelCase")]
121#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
122#[non_exhaustive]
123pub struct ElicitationCapability {
124    /// Whether client supports form-based elicitation.
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub form: Option<FormElicitationCapability>,
127    /// Whether client supports URL-based elicitation.
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub url: Option<UrlElicitationCapability>,
130}
131
132impl ElicitationCapability {
133    pub fn new() -> Self {
134        Self::default()
135    }
136
137    pub fn with_form(mut self, form: FormElicitationCapability) -> Self {
138        self.form = Some(form);
139        self
140    }
141
142    pub fn with_url(mut self, url: UrlElicitationCapability) -> Self {
143        self.url = Some(url);
144        self
145    }
146}
147
148/// Sampling capability with optional sub-capabilities (SEP-1577).
149///
150/// Deprecated by SEP-2577; remains functional and will be removed in a future
151/// release.
152/// See <https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577>.
153#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
154#[serde(rename_all = "camelCase")]
155#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
156#[non_exhaustive]
157pub struct SamplingCapability {
158    /// Support for `tools` and `toolChoice` parameters
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub tools: Option<JsonObject>,
161    /// Support for `includeContext` (soft-deprecated)
162    #[serde(skip_serializing_if = "Option::is_none")]
163    pub context: Option<JsonObject>,
164}
165
166///
167/// # Builder
168/// ```rust
169/// # use rmcp::model::ClientCapabilities;
170/// let cap = ClientCapabilities::builder()
171///     .enable_experimental()
172///     .build();
173/// ```
174#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
175#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
176#[non_exhaustive]
177pub struct ClientCapabilities {
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub experimental: Option<ExperimentalCapabilities>,
180    /// Optional MCP extensions that the client supports (SEP-1724).
181    /// Keys are extension identifiers (e.g., `"io.modelcontextprotocol/ui"`),
182    /// values are per-extension settings objects. An empty object indicates
183    /// support with no settings.
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub extensions: Option<ExtensionCapabilities>,
186    /// Capability for filesystem roots (deprecated by SEP-2577).
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub roots: Option<RootsCapabilities>,
189    /// Capability for LLM sampling requests (SEP-1577, deprecated by SEP-2577).
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub sampling: Option<SamplingCapability>,
192    /// Capability to handle elicitation requests from servers for interactive user input
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub elicitation: Option<ElicitationCapability>,
195}
196
197impl ClientCapabilities {
198    /// Returns `true` if the `io.modelcontextprotocol/tasks` extension
199    /// (SEP-2663) is declared in [`Self::extensions`].
200    pub fn supports_tasks(&self) -> bool {
201        self.extensions
202            .as_ref()
203            .is_some_and(|e| e.contains_key(super::TASKS_EXTENSION_ID))
204    }
205}
206
207///
208/// ## Builder
209/// ```rust
210/// # use rmcp::model::ServerCapabilities;
211/// let cap = ServerCapabilities::builder()
212///     .enable_experimental()
213///     .enable_prompts()
214///     .enable_resources()
215///     .enable_tools()
216///     .enable_tool_list_changed()
217///     .build();
218/// ```
219#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
220#[serde(rename_all = "camelCase")]
221#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
222#[non_exhaustive]
223pub struct ServerCapabilities {
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub experimental: Option<ExperimentalCapabilities>,
226    /// Optional MCP extensions that the server supports (SEP-1724).
227    /// Keys are extension identifiers (e.g., `"io.modelcontextprotocol/apps"`),
228    /// values are per-extension settings objects. An empty object indicates
229    /// support with no settings.
230    #[serde(skip_serializing_if = "Option::is_none")]
231    pub extensions: Option<ExtensionCapabilities>,
232    /// Capability for server log message notifications (deprecated by SEP-2577).
233    #[serde(skip_serializing_if = "Option::is_none")]
234    pub logging: Option<JsonObject>,
235    #[serde(skip_serializing_if = "Option::is_none")]
236    pub completions: Option<JsonObject>,
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub prompts: Option<PromptsCapability>,
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub resources: Option<ResourcesCapability>,
241    #[serde(skip_serializing_if = "Option::is_none")]
242    pub tools: Option<ToolsCapability>,
243}
244
245impl ServerCapabilities {
246    /// Returns `true` if the `io.modelcontextprotocol/tasks` extension
247    /// (SEP-2663) is declared in [`Self::extensions`].
248    pub fn supports_tasks(&self) -> bool {
249        self.extensions
250            .as_ref()
251            .is_some_and(|e| e.contains_key(super::TASKS_EXTENSION_ID))
252    }
253}
254
255#[cfg(any(feature = "server", feature = "macros"))]
256macro_rules! builder {
257    ($Target: ident {$($(#[$fa:meta])* $f: ident: $T: ty),* $(,)?}) => {
258        paste! {
259            #[derive(Default, Clone, Copy, Debug)]
260            #[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
261            pub struct [<$Target BuilderState>]<
262                $(const [<$f:upper>]: bool = false,)*
263            >;
264            #[derive(Debug, Default)]
265            #[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
266            pub struct [<$Target Builder>]<S = [<$Target BuilderState>]> {
267                $(pub $f: Option<$T>,)*
268                pub state: PhantomData<S>
269            }
270            impl $Target {
271                #[doc = "Create a new [`" $Target "`] builder."]
272                pub fn builder() -> [<$Target Builder>] {
273                    <[<$Target Builder>]>::default()
274                }
275            }
276            impl<S> [<$Target Builder>]<S> {
277                pub fn build(self) -> $Target {
278                    $Target {
279                        $( $f: self.$f, )*
280                    }
281                }
282            }
283            impl<S> From<[<$Target Builder>]<S>> for $Target {
284                fn from(builder: [<$Target Builder>]<S>) -> Self {
285                    builder.build()
286                }
287            }
288        }
289        builder!($Target @toggle $($(#[$fa])* $f: $T,)*);
290
291    };
292    ($Target: ident @toggle $(#[$fa0:meta])* $f0: ident: $T0: ty, $($(#[$fa:meta])* $f: ident: $T: ty,)*) => {
293        builder!($Target @toggle [][$(#[$fa0])* $f0: $T0][$($(#[$fa])* $f: $T,)*]);
294    };
295    ($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,)*]) => {
296        builder!($Target @impl_toggle [$($ff: $Tf,)*][$(#[$fna])* $fn: $TN][$fn_1: $Tn_1, $($ft:$Tt,)*]);
297        builder!($Target @toggle [$($ff: $Tf,)* $fn: $TN,][$(#[$fn1a])* $fn_1: $Tn_1][$($(#[$fta])* $ft: $Tt,)*]);
298    };
299    ($Target: ident @toggle [$($ff: ident: $Tf: ty,)*][$(#[$fna:meta])* $fn: ident: $TN: ty][]) => {
300        builder!($Target @impl_toggle [$($ff: $Tf,)*][$(#[$fna])* $fn: $TN][]);
301    };
302    ($Target: ident @impl_toggle [$($ff: ident: $Tf: ty,)*][$(#[$fna:meta])* $fn: ident: $TN: ty][$($ft: ident: $Tt: ty,)*]) => {
303        paste! {
304            impl<
305                $(const [<$ff:upper>]: bool,)*
306                $(const [<$ft:upper>]: bool,)*
307            > [<$Target Builder>]<[<$Target BuilderState>]<
308                $([<$ff:upper>],)*
309                false,
310                $([<$ft:upper>],)*
311            >> {
312                $(#[$fna])*
313                pub fn [<enable_ $fn>](self) -> [<$Target Builder>]<[<$Target BuilderState>]<
314                    $([<$ff:upper>],)*
315                    true,
316                    $([<$ft:upper>],)*
317                >> {
318                    [<$Target Builder>] {
319                        $( $ff: self.$ff, )*
320                        $fn: Some($TN::default()),
321                        $( $ft: self.$ft, )*
322                        state: PhantomData
323                    }
324                }
325                $(#[$fna])*
326                pub fn [<enable_ $fn _with>](self, $fn: $TN) -> [<$Target Builder>]<[<$Target BuilderState>]<
327                    $([<$ff:upper>],)*
328                    true,
329                    $([<$ft:upper>],)*
330                >> {
331                    [<$Target Builder>] {
332                        $( $ff: self.$ff, )*
333                        $fn: Some($fn),
334                        $( $ft: self.$ft, )*
335                        state: PhantomData
336                    }
337                }
338            }
339            // do we really need to disable some thing in builder?
340            // impl<
341            //     $(const [<$ff:upper>]: bool,)*
342            //     $(const [<$ft:upper>]: bool,)*
343            // > [<$Target Builder>]<[<$Target BuilderState>]<
344            //     $([<$ff:upper>],)*
345            //     true,
346            //     $([<$ft:upper>],)*
347            // >> {
348            //     pub fn [<disable_ $fn>](self) -> [<$Target Builder>]<[<$Target BuilderState>]<
349            //         $([<$ff:upper>],)*
350            //         false,
351            //         $([<$ft:upper>],)*
352            //     >> {
353            //         [<$Target Builder>] {
354            //             $( $ff: self.$ff, )*
355            //             $fn: None,
356            //             $( $ft: self.$ft, )*
357            //             state: PhantomData
358            //         }
359            //     }
360            // }
361        }
362    }
363}
364
365#[cfg(any(feature = "server", feature = "macros"))]
366builder! {
367    ServerCapabilities {
368        experimental: ExperimentalCapabilities,
369        extensions: ExtensionCapabilities,
370        #[deprecated(
371            since = "1.8.0",
372            note = "Logging is deprecated by SEP-2577 and will be removed in a future release. See https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577"
373        )]
374        logging: JsonObject,
375        completions: JsonObject,
376        prompts: PromptsCapability,
377        resources: ResourcesCapability,
378        tools: ToolsCapability,
379    }
380}
381
382#[cfg(any(feature = "server", feature = "macros"))]
383impl<const E: bool, const EXT: bool, const L: bool, const C: bool, const P: bool, const R: bool>
384    ServerCapabilitiesBuilder<ServerCapabilitiesBuilderState<E, EXT, L, C, P, R, true>>
385{
386    pub fn enable_tool_list_changed(mut self) -> Self {
387        if let Some(c) = self.tools.as_mut() {
388            c.list_changed = Some(true);
389        }
390        self
391    }
392}
393
394#[cfg(any(feature = "server", feature = "macros"))]
395impl<const E: bool, const EXT: bool, const L: bool, const C: bool, const R: bool, const T: bool>
396    ServerCapabilitiesBuilder<ServerCapabilitiesBuilderState<E, EXT, L, C, true, R, T>>
397{
398    pub fn enable_prompts_list_changed(mut self) -> Self {
399        if let Some(c) = self.prompts.as_mut() {
400            c.list_changed = Some(true);
401        }
402        self
403    }
404}
405
406#[cfg(any(feature = "server", feature = "macros"))]
407impl<const E: bool, const EXT: bool, const L: bool, const C: bool, const P: bool, const T: bool>
408    ServerCapabilitiesBuilder<ServerCapabilitiesBuilderState<E, EXT, L, C, P, true, T>>
409{
410    pub fn enable_resources_list_changed(mut self) -> Self {
411        if let Some(c) = self.resources.as_mut() {
412            c.list_changed = Some(true);
413        }
414        self
415    }
416
417    pub fn enable_resources_subscribe(mut self) -> Self {
418        if let Some(c) = self.resources.as_mut() {
419            c.subscribe = Some(true);
420        }
421        self
422    }
423}
424
425#[cfg(any(feature = "server", feature = "macros"))]
426impl<S> ServerCapabilitiesBuilder<S> {
427    /// Declare support for the `io.modelcontextprotocol/tasks` extension
428    /// (SEP-2663) in the `extensions` capability map.
429    pub fn enable_tasks(mut self) -> Self {
430        self.extensions
431            .get_or_insert_with(ExtensionCapabilities::new)
432            .insert(super::TASKS_EXTENSION_ID.to_string(), JsonObject::new());
433        self
434    }
435}
436
437#[cfg(any(feature = "server", feature = "macros"))]
438builder! {
439    ClientCapabilities{
440        experimental: ExperimentalCapabilities,
441        extensions: ExtensionCapabilities,
442        #[deprecated(
443            since = "1.8.0",
444            note = "Roots is deprecated by SEP-2577 and will be removed in a future release. See https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577"
445        )]
446        roots: RootsCapabilities,
447        #[deprecated(
448            since = "1.8.0",
449            note = "Sampling is deprecated by SEP-2577 and will be removed in a future release. See https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577"
450        )]
451        sampling: SamplingCapability,
452        elicitation: ElicitationCapability,
453    }
454}
455
456#[cfg(any(feature = "server", feature = "macros"))]
457impl<S> ClientCapabilitiesBuilder<S> {
458    /// Declare support for the `io.modelcontextprotocol/tasks` extension
459    /// (SEP-2663) in the `extensions` capability map.
460    pub fn enable_tasks(mut self) -> Self {
461        self.extensions
462            .get_or_insert_with(ExtensionCapabilities::new)
463            .insert(super::TASKS_EXTENSION_ID.to_string(), JsonObject::new());
464        self
465    }
466}
467
468#[cfg(any(feature = "server", feature = "macros"))]
469impl<const E: bool, const EXT: bool, const S: bool, const EL: bool>
470    ClientCapabilitiesBuilder<ClientCapabilitiesBuilderState<E, EXT, true, S, EL>>
471{
472    #[deprecated(
473        since = "1.8.0",
474        note = "Roots is deprecated by SEP-2577 and will be removed in a future release. See https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577"
475    )]
476    pub fn enable_roots_list_changed(mut self) -> Self {
477        if let Some(c) = self.roots.as_mut() {
478            c.list_changed = Some(true);
479        }
480        self
481    }
482}
483
484#[cfg(any(feature = "server", feature = "macros"))]
485impl<const E: bool, const EXT: bool, const R: bool, const EL: bool>
486    ClientCapabilitiesBuilder<ClientCapabilitiesBuilderState<E, EXT, R, true, EL>>
487{
488    /// Enable tool calling in sampling requests
489    #[deprecated(
490        since = "1.8.0",
491        note = "Sampling is deprecated by SEP-2577 and will be removed in a future release. See https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577"
492    )]
493    pub fn enable_sampling_tools(mut self) -> Self {
494        if let Some(c) = self.sampling.as_mut() {
495            c.tools = Some(JsonObject::default());
496        }
497        self
498    }
499
500    /// Enable context inclusion in sampling (soft-deprecated)
501    #[deprecated(
502        since = "1.8.0",
503        note = "Sampling is deprecated by SEP-2577 and will be removed in a future release. See https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577"
504    )]
505    pub fn enable_sampling_context(mut self) -> Self {
506        if let Some(c) = self.sampling.as_mut() {
507            c.context = Some(JsonObject::default());
508        }
509        self
510    }
511}
512
513#[cfg(all(feature = "elicitation", any(feature = "server", feature = "macros")))]
514impl<const E: bool, const EXT: bool, const R: bool, const S: bool>
515    ClientCapabilitiesBuilder<ClientCapabilitiesBuilderState<E, EXT, R, S, true>>
516{
517    /// Enable JSON Schema validation for elicitation responses in form mode.
518    /// When enabled, the client will validate user input against the requested_schema
519    /// before sending responses back to the server.
520    pub fn enable_elicitation_schema_validation(mut self) -> Self {
521        if let Some(c) = self.elicitation.as_mut() {
522            c.form = Some(FormElicitationCapability {
523                schema_validation: Some(true),
524            });
525        }
526        self
527    }
528}
529
530#[cfg(test)]
531#[cfg(any(feature = "server", feature = "macros"))]
532mod test {
533    use super::*;
534    #[test]
535    #[allow(deprecated)]
536    fn test_builder() {
537        let builder = <ServerCapabilitiesBuilder>::default()
538            .enable_logging()
539            .enable_experimental()
540            .enable_prompts()
541            .enable_resources()
542            .enable_tools()
543            .enable_tool_list_changed();
544        assert_eq!(builder.logging, Some(JsonObject::default()));
545        assert_eq!(builder.prompts, Some(PromptsCapability::default()));
546        assert_eq!(builder.resources, Some(ResourcesCapability::default()));
547        assert_eq!(
548            builder.tools,
549            Some(ToolsCapability {
550                list_changed: Some(true),
551            })
552        );
553        assert_eq!(
554            builder.experimental,
555            Some(ExperimentalCapabilities::default())
556        );
557        let client_builder = <ClientCapabilitiesBuilder>::default()
558            .enable_experimental()
559            .enable_roots()
560            .enable_roots_list_changed()
561            .enable_sampling();
562        assert_eq!(
563            client_builder.experimental,
564            Some(ExperimentalCapabilities::default())
565        );
566        assert_eq!(
567            client_builder.roots,
568            Some(RootsCapabilities {
569                list_changed: Some(true),
570            })
571        );
572    }
573
574    #[test]
575    fn test_tasks_extension_capability() {
576        // SEP-2663: tasks are declared via the extensions map.
577        let capabilities = ClientCapabilities::builder().enable_tasks().build();
578        assert!(capabilities.supports_tasks());
579        let json = serde_json::to_value(&capabilities).unwrap();
580        assert_eq!(
581            json["extensions"][crate::model::TASKS_EXTENSION_ID],
582            serde_json::json!({})
583        );
584
585        let server = ServerCapabilities::builder().enable_tasks().build();
586        assert!(server.supports_tasks());
587        let json = serde_json::to_value(&server).unwrap();
588        assert_eq!(
589            json["extensions"][crate::model::TASKS_EXTENSION_ID],
590            serde_json::json!({})
591        );
592    }
593
594    #[test]
595    #[allow(deprecated)]
596    fn test_client_extensions_capability() {
597        // Test building ClientCapabilities with extensions (MCP Apps support)
598        let mut extensions = ExtensionCapabilities::new();
599        extensions.insert(
600            "io.modelcontextprotocol/ui".to_string(),
601            serde_json::from_value(serde_json::json!({
602                "mimeTypes": ["text/html;profile=mcp-app"]
603            }))
604            .unwrap(),
605        );
606
607        let capabilities = ClientCapabilities::builder()
608            .enable_extensions_with(extensions)
609            .enable_sampling()
610            .build();
611
612        // Verify serialization matches MCP Apps spec format
613        let json = serde_json::to_value(&capabilities).unwrap();
614        assert_eq!(
615            json["extensions"]["io.modelcontextprotocol/ui"]["mimeTypes"],
616            serde_json::json!(["text/html;profile=mcp-app"])
617        );
618        assert!(json["sampling"].is_object());
619    }
620
621    #[test]
622    fn test_server_extensions_capability() {
623        // Test building ServerCapabilities with extensions
624        let mut extensions = ExtensionCapabilities::new();
625        extensions.insert(
626            "io.modelcontextprotocol/apps".to_string(),
627            serde_json::from_value(serde_json::json!({})).unwrap(),
628        );
629
630        let capabilities = ServerCapabilities::builder()
631            .enable_extensions_with(extensions)
632            .enable_tools()
633            .build();
634
635        // Verify serialization
636        let json = serde_json::to_value(&capabilities).unwrap();
637        assert!(json["extensions"]["io.modelcontextprotocol/apps"].is_object());
638        assert!(json["tools"].is_object());
639    }
640
641    #[test]
642    fn test_extensions_deserialization() {
643        // Test deserializing capabilities with extensions from JSON
644        let json = serde_json::json!({
645            "extensions": {
646                "io.modelcontextprotocol/ui": {
647                    "mimeTypes": ["text/html;profile=mcp-app"]
648                }
649            },
650            "sampling": {}
651        });
652
653        let capabilities: ClientCapabilities = serde_json::from_value(json).unwrap();
654        assert!(capabilities.extensions.is_some());
655        let extensions = capabilities.extensions.unwrap();
656        assert!(extensions.contains_key("io.modelcontextprotocol/ui"));
657        let ui_ext = extensions.get("io.modelcontextprotocol/ui").unwrap();
658        assert!(ui_ext.contains_key("mimeTypes"));
659    }
660
661    #[test]
662    fn test_extensions_empty_settings() {
663        // Test that empty extension settings work (indicates support with no settings)
664        let mut extensions = ExtensionCapabilities::new();
665        extensions.insert(
666            "io.modelcontextprotocol/oauth-client-credentials".to_string(),
667            JsonObject::new(),
668        );
669
670        let capabilities = ClientCapabilities::builder()
671            .enable_extensions_with(extensions)
672            .build();
673
674        let json = serde_json::to_value(&capabilities).unwrap();
675        assert_eq!(
676            json["extensions"]["io.modelcontextprotocol/oauth-client-credentials"],
677            serde_json::json!({})
678        );
679    }
680}