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
12pub 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#[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#[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 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 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#[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 #[serde(skip_serializing_if = "Option::is_none")]
208 pub schema_validation: Option<bool>,
209}
210
211#[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#[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 #[serde(skip_serializing_if = "Option::is_none")]
227 pub form: Option<FormElicitationCapability>,
228 #[serde(skip_serializing_if = "Option::is_none")]
230 pub url: Option<UrlElicitationCapability>,
231}
232
233#[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 #[serde(skip_serializing_if = "Option::is_none")]
241 pub tools: Option<JsonObject>,
242 #[serde(skip_serializing_if = "Option::is_none")]
244 pub context: Option<JsonObject>,
245}
246
247#[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 #[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 #[serde(skip_serializing_if = "Option::is_none")]
273 pub sampling: Option<SamplingCapability>,
274 #[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#[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 #[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 }
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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}