pmcp/types/capabilities.rs
1//! Capability definitions for MCP clients and servers.
2//!
3//! This module defines the capability structures that clients and servers
4//! use to advertise their supported features.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// Client capabilities advertised during initialization.
10///
11/// # Examples
12///
13/// ```rust
14/// use pmcp::types::ClientCapabilities;
15///
16/// let capabilities = ClientCapabilities {
17/// experimental: Some([("custom-feature".to_string(), serde_json::json!(true))]
18/// .into_iter()
19/// .collect()),
20/// ..Default::default()
21/// };
22/// ```
23#[derive(Debug, Clone, Default, Serialize, Deserialize)]
24#[serde(rename_all = "camelCase")]
25pub struct ClientCapabilities {
26 /// Tool calling capabilities
27 #[serde(skip_serializing_if = "Option::is_none")]
28 pub tools: Option<ToolCapabilities>,
29
30 /// Prompt capabilities
31 #[serde(skip_serializing_if = "Option::is_none")]
32 pub prompts: Option<PromptCapabilities>,
33
34 /// Resource capabilities
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub resources: Option<ResourceCapabilities>,
37
38 /// Logging capabilities
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub logging: Option<LoggingCapabilities>,
41
42 /// Sampling capabilities (for LLM providers)
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub sampling: Option<SamplingCapabilities>,
45
46 /// Roots capabilities
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub roots: Option<RootsCapabilities>,
49
50 /// Experimental capabilities
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub experimental: Option<HashMap<String, serde_json::Value>>,
53}
54
55/// Server capabilities advertised during initialization.
56#[derive(Debug, Clone, Default, Serialize, Deserialize)]
57#[serde(rename_all = "camelCase")]
58pub struct ServerCapabilities {
59 /// Tool providing capabilities
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub tools: Option<ToolCapabilities>,
62
63 /// Prompt providing capabilities
64 #[serde(skip_serializing_if = "Option::is_none")]
65 pub prompts: Option<PromptCapabilities>,
66
67 /// Resource providing capabilities
68 #[serde(skip_serializing_if = "Option::is_none")]
69 pub resources: Option<ResourceCapabilities>,
70
71 /// Logging capabilities
72 #[serde(skip_serializing_if = "Option::is_none")]
73 pub logging: Option<LoggingCapabilities>,
74
75 /// Completion capabilities
76 #[serde(skip_serializing_if = "Option::is_none")]
77 pub completions: Option<CompletionCapabilities>,
78
79 /// Sampling capabilities (for LLM providers)
80 #[serde(skip_serializing_if = "Option::is_none")]
81 pub sampling: Option<SamplingCapabilities>,
82
83 /// Experimental capabilities
84 #[serde(skip_serializing_if = "Option::is_none")]
85 pub experimental: Option<HashMap<String, serde_json::Value>>,
86}
87
88/// Tool-related capabilities.
89#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
90#[serde(rename_all = "camelCase")]
91pub struct ToolCapabilities {
92 /// Whether list changes are supported
93 #[serde(skip_serializing_if = "Option::is_none")]
94 pub list_changed: Option<bool>,
95}
96
97/// Prompt-related capabilities.
98#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
99#[serde(rename_all = "camelCase")]
100pub struct PromptCapabilities {
101 /// Whether list changes are supported
102 #[serde(skip_serializing_if = "Option::is_none")]
103 pub list_changed: Option<bool>,
104}
105
106/// Resource-related capabilities.
107#[derive(Debug, Clone, Default, Serialize, Deserialize)]
108#[serde(rename_all = "camelCase")]
109pub struct ResourceCapabilities {
110 /// Whether resource subscriptions are supported
111 #[serde(skip_serializing_if = "Option::is_none")]
112 pub subscribe: Option<bool>,
113
114 /// Whether list changes are supported
115 #[serde(skip_serializing_if = "Option::is_none")]
116 pub list_changed: Option<bool>,
117}
118
119/// Logging capabilities.
120#[derive(Debug, Clone, Default, Serialize, Deserialize)]
121#[serde(rename_all = "camelCase")]
122pub struct LoggingCapabilities {
123 /// Supported log levels
124 #[serde(skip_serializing_if = "Option::is_none")]
125 pub levels: Option<Vec<String>>,
126}
127
128/// Sampling capabilities for LLM operations.
129#[derive(Debug, Clone, Default, Serialize, Deserialize)]
130#[serde(rename_all = "camelCase")]
131pub struct SamplingCapabilities {
132 /// Supported model families/providers
133 #[serde(skip_serializing_if = "Option::is_none")]
134 pub models: Option<Vec<String>>,
135}
136
137/// Roots capabilities.
138#[derive(Debug, Clone, Default, Serialize, Deserialize)]
139#[serde(rename_all = "camelCase")]
140pub struct RootsCapabilities {
141 /// Whether list changed notifications are supported
142 #[serde(default)]
143 pub list_changed: bool,
144}
145
146/// Completion capabilities.
147#[derive(Debug, Clone, Default, Serialize, Deserialize)]
148#[serde(rename_all = "camelCase")]
149pub struct CompletionCapabilities {
150 /// Placeholder for completion capability options
151 #[serde(skip)]
152 _reserved: (),
153}
154
155impl ClientCapabilities {
156 /// Create a minimal set of client capabilities.
157 ///
158 /// # Examples
159 ///
160 /// ```rust
161 /// use pmcp::ClientCapabilities;
162 ///
163 /// // Create minimal capabilities (no features advertised)
164 /// let capabilities = ClientCapabilities::minimal();
165 /// assert!(!capabilities.supports_tools());
166 /// assert!(!capabilities.supports_prompts());
167 /// assert!(!capabilities.supports_resources());
168 /// assert!(!capabilities.supports_sampling());
169 ///
170 /// // Use in client initialization
171 /// # use pmcp::{Client, StdioTransport};
172 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
173 /// let transport = StdioTransport::new();
174 /// let mut client = Client::new(transport);
175 /// let server_info = client.initialize(ClientCapabilities::minimal()).await?;
176 /// # Ok(())
177 /// # }
178 /// ```
179 pub fn minimal() -> Self {
180 Self::default()
181 }
182
183 /// Create a full set of client capabilities.
184 ///
185 /// # Examples
186 ///
187 /// ```rust
188 /// use pmcp::ClientCapabilities;
189 ///
190 /// // Create full capabilities (all features supported)
191 /// let capabilities = ClientCapabilities::full();
192 /// assert!(capabilities.supports_tools());
193 /// assert!(capabilities.supports_prompts());
194 /// assert!(capabilities.supports_resources());
195 /// assert!(capabilities.supports_sampling());
196 ///
197 /// // Inspect specific capabilities
198 /// assert!(capabilities.tools.unwrap().list_changed.unwrap());
199 /// assert!(capabilities.resources.unwrap().subscribe.unwrap());
200 ///
201 /// // Use in client that needs all features
202 /// # use pmcp::{Client, StdioTransport};
203 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
204 /// let transport = StdioTransport::new();
205 /// let mut client = Client::new(transport);
206 /// let server_info = client.initialize(ClientCapabilities::full()).await?;
207 /// # Ok(())
208 /// # }
209 /// ```
210 pub fn full() -> Self {
211 Self {
212 tools: Some(ToolCapabilities {
213 list_changed: Some(true),
214 }),
215 prompts: Some(PromptCapabilities {
216 list_changed: Some(true),
217 }),
218 resources: Some(ResourceCapabilities {
219 subscribe: Some(true),
220 list_changed: Some(true),
221 }),
222 logging: Some(LoggingCapabilities {
223 levels: Some(vec![
224 "debug".to_string(),
225 "info".to_string(),
226 "warning".to_string(),
227 "error".to_string(),
228 ]),
229 }),
230 sampling: Some(SamplingCapabilities::default()),
231 roots: Some(RootsCapabilities { list_changed: true }),
232 experimental: None,
233 }
234 }
235
236 /// Check if the client supports tools.
237 ///
238 /// # Examples
239 ///
240 /// ```rust
241 /// use pmcp::{ClientCapabilities, types::capabilities::ToolCapabilities};
242 ///
243 /// // Minimal capabilities don't support tools
244 /// let minimal = ClientCapabilities::minimal();
245 /// assert!(!minimal.supports_tools());
246 ///
247 /// // Full capabilities support tools
248 /// let full = ClientCapabilities::full();
249 /// assert!(full.supports_tools());
250 ///
251 /// // Custom capabilities with only tools
252 /// let tools_only = ClientCapabilities {
253 /// tools: Some(ToolCapabilities {
254 /// list_changed: Some(true),
255 /// }),
256 /// ..Default::default()
257 /// };
258 /// assert!(tools_only.supports_tools());
259 ///
260 /// // Use to conditionally enable features
261 /// fn setup_client(caps: &ClientCapabilities) {
262 /// if caps.supports_tools() {
263 /// println!("Client can call tools");
264 /// }
265 /// }
266 /// ```
267 pub fn supports_tools(&self) -> bool {
268 self.tools.is_some()
269 }
270
271 /// Check if the client supports prompts.
272 ///
273 /// # Examples
274 ///
275 /// ```rust
276 /// use pmcp::{ClientCapabilities, types::capabilities::PromptCapabilities};
277 ///
278 /// // Check prompt support
279 /// let caps = ClientCapabilities::full();
280 /// assert!(caps.supports_prompts());
281 ///
282 /// // Build capabilities with just prompts
283 /// let prompts_only = ClientCapabilities {
284 /// prompts: Some(PromptCapabilities {
285 /// list_changed: Some(true),
286 /// }),
287 /// ..Default::default()
288 /// };
289 /// assert!(prompts_only.supports_prompts());
290 /// assert!(!prompts_only.supports_tools());
291 ///
292 /// // Conditional logic based on prompt support
293 /// # use pmcp::{Client, StdioTransport};
294 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
295 /// let caps = ClientCapabilities::full();
296 /// if caps.supports_prompts() {
297 /// // Client can use prompts
298 /// println!("This client supports prompts");
299 /// }
300 /// # Ok(())
301 /// # }
302 /// ```
303 pub fn supports_prompts(&self) -> bool {
304 self.prompts.is_some()
305 }
306
307 /// Check if the client supports resources.
308 ///
309 /// # Examples
310 ///
311 /// ```rust
312 /// use pmcp::{ClientCapabilities, types::capabilities::ResourceCapabilities};
313 ///
314 /// // Check resource support with subscriptions
315 /// let caps = ClientCapabilities::full();
316 /// assert!(caps.supports_resources());
317 ///
318 /// // Build capabilities with advanced resource features
319 /// let advanced_resources = ClientCapabilities {
320 /// resources: Some(ResourceCapabilities {
321 /// subscribe: Some(true),
322 /// list_changed: Some(true),
323 /// }),
324 /// ..Default::default()
325 /// };
326 /// assert!(advanced_resources.supports_resources());
327 ///
328 /// // Check specific resource capabilities
329 /// if let Some(resource_caps) = &advanced_resources.resources {
330 /// assert!(resource_caps.subscribe.unwrap_or(false));
331 /// println!("Client can subscribe to resource changes");
332 /// }
333 /// ```
334 pub fn supports_resources(&self) -> bool {
335 self.resources.is_some()
336 }
337
338 /// Check if the client supports sampling.
339 ///
340 /// # Examples
341 ///
342 /// ```rust
343 /// use pmcp::{ClientCapabilities, types::capabilities::SamplingCapabilities};
344 ///
345 /// // Check sampling support for LLM operations
346 /// let caps = ClientCapabilities::full();
347 /// assert!(caps.supports_sampling());
348 ///
349 /// // Build LLM client capabilities
350 /// let llm_client = ClientCapabilities {
351 /// sampling: Some(SamplingCapabilities {
352 /// models: Some(vec![
353 /// "gpt-4".to_string(),
354 /// "claude-3".to_string(),
355 /// "llama-2".to_string(),
356 /// ]),
357 /// }),
358 /// ..Default::default()
359 /// };
360 /// assert!(llm_client.supports_sampling());
361 ///
362 /// // List supported models
363 /// if let Some(sampling) = &llm_client.sampling {
364 /// if let Some(models) = &sampling.models {
365 /// println!("Supported models: {:?}", models);
366 /// }
367 /// }
368 /// ```
369 pub fn supports_sampling(&self) -> bool {
370 self.sampling.is_some()
371 }
372}
373
374impl ServerCapabilities {
375 /// Create a minimal set of server capabilities.
376 ///
377 /// # Examples
378 ///
379 /// ```rust
380 /// use pmcp::ServerCapabilities;
381 ///
382 /// // Create minimal server with no advertised features
383 /// let capabilities = ServerCapabilities::minimal();
384 /// assert!(!capabilities.provides_tools());
385 /// assert!(!capabilities.provides_prompts());
386 /// assert!(!capabilities.provides_resources());
387 ///
388 /// // Use in server that implements custom protocol extensions
389 /// # use pmcp::Server;
390 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
391 /// let server = Server::builder()
392 /// .name("minimal-server")
393 /// .version("1.0.0")
394 /// .capabilities(ServerCapabilities::minimal())
395 /// .build()?;
396 /// # Ok(())
397 /// # }
398 /// ```
399 pub fn minimal() -> Self {
400 Self::default()
401 }
402
403 /// Create capabilities for a tool server.
404 ///
405 /// # Examples
406 ///
407 /// ```rust
408 /// use pmcp::ServerCapabilities;
409 ///
410 /// // Create server that only provides tools
411 /// let capabilities = ServerCapabilities::tools_only();
412 /// assert!(capabilities.provides_tools());
413 /// assert!(!capabilities.provides_prompts());
414 /// assert!(!capabilities.provides_resources());
415 ///
416 /// // Use in a tool-focused server
417 /// # use pmcp::{Server, ToolHandler};
418 /// # use async_trait::async_trait;
419 /// # struct CalculatorTool;
420 /// # #[async_trait]
421 /// # impl ToolHandler for CalculatorTool {
422 /// # async fn handle(&self, args: serde_json::Value, _extra: pmcp::RequestHandlerExtra) -> Result<serde_json::Value, pmcp::Error> {
423 /// # Ok(serde_json::json!({"result": 42}))
424 /// # }
425 /// # }
426 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
427 /// let server = Server::builder()
428 /// .name("calculator-server")
429 /// .version("1.0.0")
430 /// .capabilities(ServerCapabilities::tools_only())
431 /// .tool("calculate", CalculatorTool)
432 /// .build()?;
433 /// # Ok(())
434 /// # }
435 /// ```
436 pub fn tools_only() -> Self {
437 Self {
438 tools: Some(ToolCapabilities {
439 list_changed: Some(true),
440 }),
441 ..Default::default()
442 }
443 }
444
445 /// Create capabilities for a prompt server.
446 ///
447 /// # Examples
448 ///
449 /// ```rust
450 /// use pmcp::ServerCapabilities;
451 ///
452 /// // Create server that only provides prompts
453 /// let capabilities = ServerCapabilities::prompts_only();
454 /// assert!(!capabilities.provides_tools());
455 /// assert!(capabilities.provides_prompts());
456 /// assert!(!capabilities.provides_resources());
457 ///
458 /// // Use in a prompt template server
459 /// # use pmcp::{Server, PromptHandler};
460 /// # use async_trait::async_trait;
461 /// # use pmcp::types::protocol::{GetPromptResult, PromptMessage, Role, Content};
462 /// # struct GreetingPrompt;
463 /// # #[async_trait]
464 /// # impl PromptHandler for GreetingPrompt {
465 /// # async fn handle(&self, args: std::collections::HashMap<String, String>, _extra: pmcp::RequestHandlerExtra) -> Result<GetPromptResult, pmcp::Error> {
466 /// # Ok(GetPromptResult {
467 /// # description: Some("Greeting prompt".to_string()),
468 /// # messages: vec![PromptMessage {
469 /// # role: Role::System,
470 /// # content: Content::Text { text: "Hello!".to_string() },
471 /// # }],
472 /// # })
473 /// # }
474 /// # }
475 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
476 /// let server = Server::builder()
477 /// .name("prompt-server")
478 /// .version("1.0.0")
479 /// .capabilities(ServerCapabilities::prompts_only())
480 /// .prompt("greeting", GreetingPrompt)
481 /// .build()?;
482 /// # Ok(())
483 /// # }
484 /// ```
485 pub fn prompts_only() -> Self {
486 Self {
487 prompts: Some(PromptCapabilities {
488 list_changed: Some(true),
489 }),
490 ..Default::default()
491 }
492 }
493
494 /// Create capabilities for a resource server.
495 ///
496 /// # Examples
497 ///
498 /// ```rust
499 /// use pmcp::ServerCapabilities;
500 ///
501 /// // Create server that only provides resources
502 /// let capabilities = ServerCapabilities::resources_only();
503 /// assert!(!capabilities.provides_tools());
504 /// assert!(!capabilities.provides_prompts());
505 /// assert!(capabilities.provides_resources());
506 ///
507 /// // Check subscription support
508 /// let resource_caps = capabilities.resources.unwrap();
509 /// assert!(resource_caps.subscribe.unwrap());
510 /// assert!(resource_caps.list_changed.unwrap());
511 ///
512 /// // Use in a file system resource server
513 /// # use pmcp::{Server, ResourceHandler};
514 /// # use async_trait::async_trait;
515 /// # use pmcp::types::protocol::{ReadResourceResult, ListResourcesResult, ResourceInfo, Content};
516 /// # struct FileResource;
517 /// # #[async_trait]
518 /// # impl ResourceHandler for FileResource {
519 /// # async fn read(&self, uri: &str, _extra: pmcp::RequestHandlerExtra) -> Result<ReadResourceResult, pmcp::Error> {
520 /// # Ok(ReadResourceResult {
521 /// # contents: vec![Content::Text { text: "File contents".to_string() }],
522 /// # })
523 /// # }
524 /// # async fn list(&self, _path: Option<String>, _extra: pmcp::RequestHandlerExtra) -> Result<ListResourcesResult, pmcp::Error> {
525 /// # Ok(ListResourcesResult {
526 /// # resources: vec![],
527 /// # next_cursor: None,
528 /// # })
529 /// # }
530 /// # }
531 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
532 /// let server = Server::builder()
533 /// .name("filesystem-server")
534 /// .version("1.0.0")
535 /// .capabilities(ServerCapabilities::resources_only())
536 /// .resources(FileResource)
537 /// .build()?;
538 /// # Ok(())
539 /// # }
540 /// ```
541 pub fn resources_only() -> Self {
542 Self {
543 resources: Some(ResourceCapabilities {
544 subscribe: Some(true),
545 list_changed: Some(true),
546 }),
547 ..Default::default()
548 }
549 }
550
551 /// Check if the server provides tools.
552 ///
553 /// # Examples
554 ///
555 /// ```rust
556 /// use pmcp::ServerCapabilities;
557 ///
558 /// // Check different server configurations
559 /// let tool_server = ServerCapabilities::tools_only();
560 /// assert!(tool_server.provides_tools());
561 ///
562 /// let minimal_server = ServerCapabilities::minimal();
563 /// assert!(!minimal_server.provides_tools());
564 ///
565 /// // Use in server logic
566 /// fn validate_server(caps: &ServerCapabilities) {
567 /// if caps.provides_tools() {
568 /// println!("Server can handle tool calls");
569 /// } else {
570 /// println!("Server does not provide tools");
571 /// }
572 /// }
573 ///
574 /// // Combine multiple capabilities
575 /// use pmcp::types::capabilities::{ToolCapabilities, PromptCapabilities};
576 /// let multi_server = ServerCapabilities {
577 /// tools: Some(ToolCapabilities::default()),
578 /// prompts: Some(PromptCapabilities::default()),
579 /// ..Default::default()
580 /// };
581 /// assert!(multi_server.provides_tools());
582 /// assert!(multi_server.provides_prompts());
583 /// ```
584 pub fn provides_tools(&self) -> bool {
585 self.tools.is_some()
586 }
587
588 /// Check if the server provides prompts.
589 ///
590 /// # Examples
591 ///
592 /// ```rust
593 /// use pmcp::ServerCapabilities;
594 ///
595 /// // Check prompt server
596 /// let prompt_server = ServerCapabilities::prompts_only();
597 /// assert!(prompt_server.provides_prompts());
598 /// assert!(!prompt_server.provides_tools());
599 ///
600 /// // Use in client code to check server features
601 /// # use pmcp::{Client, StdioTransport};
602 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
603 /// # let transport = StdioTransport::new();
604 /// # let mut client = Client::new(transport);
605 /// # let server_info = client.initialize(pmcp::ClientCapabilities::default()).await?;
606 /// if server_info.capabilities.provides_prompts() {
607 /// // Server supports prompts, we can list them
608 /// let prompts = client.list_prompts(None).await?;
609 /// println!("Available prompts: {}", prompts.prompts.len());
610 /// }
611 /// # Ok(())
612 /// # }
613 /// ```
614 pub fn provides_prompts(&self) -> bool {
615 self.prompts.is_some()
616 }
617
618 /// Check if the server provides resources.
619 ///
620 /// # Examples
621 ///
622 /// ```rust
623 /// use pmcp::ServerCapabilities;
624 ///
625 /// // Check resource server capabilities
626 /// let resource_server = ServerCapabilities::resources_only();
627 /// assert!(resource_server.provides_resources());
628 ///
629 /// // Check if subscriptions are supported
630 /// if resource_server.provides_resources() {
631 /// if let Some(res_caps) = &resource_server.resources {
632 /// if res_caps.subscribe.unwrap_or(false) {
633 /// println!("Server supports resource subscriptions");
634 /// }
635 /// }
636 /// }
637 ///
638 /// // Build a full-featured server
639 /// use pmcp::types::capabilities::*;
640 /// let full_server = ServerCapabilities {
641 /// tools: Some(ToolCapabilities::default()),
642 /// prompts: Some(PromptCapabilities::default()),
643 /// resources: Some(ResourceCapabilities {
644 /// subscribe: Some(true),
645 /// list_changed: Some(true),
646 /// }),
647 /// ..Default::default()
648 /// };
649 /// assert!(full_server.provides_tools());
650 /// assert!(full_server.provides_prompts());
651 /// assert!(full_server.provides_resources());
652 /// ```
653 pub fn provides_resources(&self) -> bool {
654 self.resources.is_some()
655 }
656}
657
658#[cfg(test)]
659mod tests {
660 use super::*;
661
662 #[test]
663 fn client_capabilities_helpers() {
664 let minimal = ClientCapabilities::minimal();
665 assert!(!minimal.supports_tools());
666 assert!(!minimal.supports_prompts());
667 assert!(!minimal.supports_resources());
668 assert!(!minimal.supports_sampling());
669
670 let full = ClientCapabilities::full();
671 assert!(full.supports_tools());
672 assert!(full.supports_prompts());
673 assert!(full.supports_resources());
674 assert!(full.supports_sampling());
675 }
676
677 #[test]
678 fn server_capabilities_helpers() {
679 let tools_only = ServerCapabilities::tools_only();
680 assert!(tools_only.provides_tools());
681 assert!(!tools_only.provides_prompts());
682 assert!(!tools_only.provides_resources());
683
684 let prompts_only = ServerCapabilities::prompts_only();
685 assert!(!prompts_only.provides_tools());
686 assert!(prompts_only.provides_prompts());
687 assert!(!prompts_only.provides_resources());
688 }
689
690 #[test]
691 fn capabilities_serialization() {
692 let caps = ClientCapabilities {
693 tools: Some(ToolCapabilities {
694 list_changed: Some(true),
695 }),
696 ..Default::default()
697 };
698
699 let json = serde_json::to_value(&caps).unwrap();
700 assert_eq!(json["tools"]["listChanged"], true);
701 assert!(json.get("prompts").is_none());
702 }
703}