Skip to main content

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