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#[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#[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#[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 #[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#[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#[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 #[serde(skip_serializing_if = "Option::is_none")]
126 pub form: Option<FormElicitationCapability>,
127 #[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#[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 #[serde(skip_serializing_if = "Option::is_none")]
160 pub tools: Option<JsonObject>,
161 #[serde(skip_serializing_if = "Option::is_none")]
163 pub context: Option<JsonObject>,
164}
165
166#[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 #[serde(skip_serializing_if = "Option::is_none")]
185 pub extensions: Option<ExtensionCapabilities>,
186 #[serde(skip_serializing_if = "Option::is_none")]
188 pub roots: Option<RootsCapabilities>,
189 #[serde(skip_serializing_if = "Option::is_none")]
191 pub sampling: Option<SamplingCapability>,
192 #[serde(skip_serializing_if = "Option::is_none")]
194 pub elicitation: Option<ElicitationCapability>,
195}
196
197impl ClientCapabilities {
198 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#[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 #[serde(skip_serializing_if = "Option::is_none")]
231 pub extensions: Option<ExtensionCapabilities>,
232 #[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 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 }
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 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 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 #[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 #[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 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 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 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 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 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 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 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 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}