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