mcpkit_core/
capability.rs

1//! Capability flags for MCP clients and servers.
2//!
3//! Capabilities are negotiated during the initialization handshake.
4//! They determine what features are available in the session.
5
6use serde::{Deserialize, Serialize};
7
8/// Server capabilities advertised during initialization.
9#[derive(Debug, Clone, Default, Serialize, Deserialize)]
10pub struct ServerCapabilities {
11    /// Tool capabilities.
12    #[serde(skip_serializing_if = "Option::is_none")]
13    pub tools: Option<ToolCapability>,
14    /// Resource capabilities.
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub resources: Option<ResourceCapability>,
17    /// Prompt capabilities.
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub prompts: Option<PromptCapability>,
20    /// Task capabilities.
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub tasks: Option<TaskCapability>,
23    /// Logging capabilities.
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub logging: Option<LoggingCapability>,
26    /// Completion capabilities.
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub completions: Option<CompletionCapability>,
29    /// Experimental capabilities.
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub experimental: Option<serde_json::Value>,
32}
33
34impl ServerCapabilities {
35    /// Create empty capabilities.
36    #[must_use]
37    pub fn new() -> Self {
38        Self::default()
39    }
40
41    /// Enable tool support.
42    #[must_use]
43    pub fn with_tools(mut self) -> Self {
44        self.tools = Some(ToolCapability::default());
45        self
46    }
47
48    /// Enable tool support with change notifications.
49    #[must_use]
50    pub const fn with_tools_and_changes(mut self) -> Self {
51        self.tools = Some(ToolCapability {
52            list_changed: Some(true),
53        });
54        self
55    }
56
57    /// Enable resource support.
58    #[must_use]
59    pub fn with_resources(mut self) -> Self {
60        self.resources = Some(ResourceCapability::default());
61        self
62    }
63
64    /// Enable resource support with subscriptions.
65    #[must_use]
66    pub const fn with_resources_and_subscriptions(mut self) -> Self {
67        self.resources = Some(ResourceCapability {
68            subscribe: Some(true),
69            list_changed: Some(true),
70        });
71        self
72    }
73
74    /// Enable prompt support.
75    #[must_use]
76    pub fn with_prompts(mut self) -> Self {
77        self.prompts = Some(PromptCapability::default());
78        self
79    }
80
81    /// Enable task support.
82    #[must_use]
83    pub fn with_tasks(mut self) -> Self {
84        self.tasks = Some(TaskCapability::default());
85        self
86    }
87
88    /// Enable logging support.
89    #[must_use]
90    pub const fn with_logging(mut self) -> Self {
91        self.logging = Some(LoggingCapability {});
92        self
93    }
94
95    /// Enable completion support.
96    #[must_use]
97    pub const fn with_completions(mut self) -> Self {
98        self.completions = Some(CompletionCapability {});
99        self
100    }
101
102    /// Check if tools are supported.
103    #[must_use]
104    pub const fn has_tools(&self) -> bool {
105        self.tools.is_some()
106    }
107
108    /// Check if resources are supported.
109    #[must_use]
110    pub const fn has_resources(&self) -> bool {
111        self.resources.is_some()
112    }
113
114    /// Check if prompts are supported.
115    #[must_use]
116    pub const fn has_prompts(&self) -> bool {
117        self.prompts.is_some()
118    }
119
120    /// Check if tasks are supported.
121    #[must_use]
122    pub const fn has_tasks(&self) -> bool {
123        self.tasks.is_some()
124    }
125
126    /// Check if completions are supported.
127    #[must_use]
128    pub const fn has_completions(&self) -> bool {
129        self.completions.is_some()
130    }
131
132    /// Check if resource subscriptions are supported.
133    #[must_use]
134    pub fn has_resource_subscribe(&self) -> bool {
135        self.resources
136            .as_ref()
137            .and_then(|r| r.subscribe)
138            .unwrap_or(false)
139    }
140}
141
142/// Client capabilities advertised during initialization.
143#[derive(Debug, Clone, Default, Serialize, Deserialize)]
144pub struct ClientCapabilities {
145    /// Roots (file system access) capabilities.
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub roots: Option<RootsCapability>,
148    /// Sampling capabilities.
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub sampling: Option<SamplingCapability>,
151    /// Elicitation capabilities.
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub elicitation: Option<ElicitationCapability>,
154    /// Experimental capabilities.
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub experimental: Option<serde_json::Value>,
157}
158
159impl ClientCapabilities {
160    /// Create empty capabilities.
161    #[must_use]
162    pub fn new() -> Self {
163        Self::default()
164    }
165
166    /// Enable roots support.
167    #[must_use]
168    pub fn with_roots(mut self) -> Self {
169        self.roots = Some(RootsCapability::default());
170        self
171    }
172
173    /// Enable roots support with change notifications.
174    #[must_use]
175    pub const fn with_roots_and_changes(mut self) -> Self {
176        self.roots = Some(RootsCapability {
177            list_changed: Some(true),
178        });
179        self
180    }
181
182    /// Enable sampling support.
183    #[must_use]
184    pub const fn with_sampling(mut self) -> Self {
185        self.sampling = Some(SamplingCapability {});
186        self
187    }
188
189    /// Enable elicitation support.
190    #[must_use]
191    pub const fn with_elicitation(mut self) -> Self {
192        self.elicitation = Some(ElicitationCapability {});
193        self
194    }
195
196    /// Check if roots are supported.
197    #[must_use]
198    pub const fn has_roots(&self) -> bool {
199        self.roots.is_some()
200    }
201
202    /// Check if sampling is supported.
203    #[must_use]
204    pub const fn has_sampling(&self) -> bool {
205        self.sampling.is_some()
206    }
207
208    /// Check if elicitation is supported.
209    #[must_use]
210    pub const fn has_elicitation(&self) -> bool {
211        self.elicitation.is_some()
212    }
213}
214
215/// Tool capability flags.
216#[derive(Debug, Clone, Default, Serialize, Deserialize)]
217pub struct ToolCapability {
218    /// If true, the server will send tool list changed notifications.
219    #[serde(rename = "listChanged", skip_serializing_if = "Option::is_none")]
220    pub list_changed: Option<bool>,
221}
222
223/// Resource capability flags.
224#[derive(Debug, Clone, Default, Serialize, Deserialize)]
225pub struct ResourceCapability {
226    /// If true, the server supports resource subscriptions.
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub subscribe: Option<bool>,
229    /// If true, the server will send resource list changed notifications.
230    #[serde(rename = "listChanged", skip_serializing_if = "Option::is_none")]
231    pub list_changed: Option<bool>,
232}
233
234/// Prompt capability flags.
235#[derive(Debug, Clone, Default, Serialize, Deserialize)]
236pub struct PromptCapability {
237    /// If true, the server will send prompt list changed notifications.
238    #[serde(rename = "listChanged", skip_serializing_if = "Option::is_none")]
239    pub list_changed: Option<bool>,
240}
241
242/// Task capability flags.
243#[derive(Debug, Clone, Default, Serialize, Deserialize)]
244pub struct TaskCapability {
245    /// If true, the server supports task cancellation.
246    #[serde(skip_serializing_if = "Option::is_none")]
247    pub cancellable: Option<bool>,
248}
249
250/// Logging capability flags.
251#[derive(Debug, Clone, Default, Serialize, Deserialize)]
252pub struct LoggingCapability {}
253
254/// Completion capability flags.
255#[derive(Debug, Clone, Default, Serialize, Deserialize)]
256pub struct CompletionCapability {}
257
258/// Roots capability flags.
259#[derive(Debug, Clone, Default, Serialize, Deserialize)]
260pub struct RootsCapability {
261    /// If true, the client will send roots list changed notifications.
262    #[serde(rename = "listChanged", skip_serializing_if = "Option::is_none")]
263    pub list_changed: Option<bool>,
264}
265
266/// Sampling capability flags.
267#[derive(Debug, Clone, Default, Serialize, Deserialize)]
268pub struct SamplingCapability {}
269
270/// Elicitation capability flags.
271#[derive(Debug, Clone, Default, Serialize, Deserialize)]
272pub struct ElicitationCapability {}
273
274/// Server information provided during initialization.
275#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct ServerInfo {
277    /// Server name.
278    pub name: String,
279    /// Server version.
280    pub version: String,
281    /// Protocol version supported.
282    #[serde(rename = "protocolVersion", skip_serializing_if = "Option::is_none")]
283    pub protocol_version: Option<String>,
284}
285
286impl ServerInfo {
287    /// Create new server info.
288    #[must_use]
289    pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
290        Self {
291            name: name.into(),
292            version: version.into(),
293            protocol_version: Some(PROTOCOL_VERSION.to_string()),
294        }
295    }
296}
297
298/// Client information provided during initialization.
299#[derive(Debug, Clone, Serialize, Deserialize)]
300pub struct ClientInfo {
301    /// Client name.
302    pub name: String,
303    /// Client version.
304    pub version: String,
305}
306
307impl ClientInfo {
308    /// Create new client info.
309    #[must_use]
310    pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
311        Self {
312            name: name.into(),
313            version: version.into(),
314        }
315    }
316}
317
318/// Initialize request parameters.
319#[derive(Debug, Clone, Serialize, Deserialize)]
320pub struct InitializeRequest {
321    /// Protocol version the client supports.
322    #[serde(rename = "protocolVersion")]
323    pub protocol_version: String,
324    /// Client capabilities.
325    pub capabilities: ClientCapabilities,
326    /// Client information.
327    #[serde(rename = "clientInfo")]
328    pub client_info: ClientInfo,
329}
330
331impl InitializeRequest {
332    /// Create a new initialize request.
333    #[must_use]
334    pub fn new(client_info: ClientInfo, capabilities: ClientCapabilities) -> Self {
335        Self {
336            protocol_version: PROTOCOL_VERSION.to_string(),
337            capabilities,
338            client_info,
339        }
340    }
341}
342
343/// Initialize response.
344#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct InitializeResult {
346    /// Protocol version the server supports.
347    #[serde(rename = "protocolVersion")]
348    pub protocol_version: String,
349    /// Server capabilities.
350    pub capabilities: ServerCapabilities,
351    /// Server information.
352    #[serde(rename = "serverInfo")]
353    pub server_info: ServerInfo,
354    /// Optional instructions for using this server.
355    #[serde(skip_serializing_if = "Option::is_none")]
356    pub instructions: Option<String>,
357}
358
359impl InitializeResult {
360    /// Create a new initialize result.
361    #[must_use]
362    pub fn new(server_info: ServerInfo, capabilities: ServerCapabilities) -> Self {
363        Self {
364            protocol_version: PROTOCOL_VERSION.to_string(),
365            capabilities,
366            server_info,
367            instructions: None,
368        }
369    }
370
371    /// Set instructions.
372    #[must_use]
373    pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
374        self.instructions = Some(instructions.into());
375        self
376    }
377}
378
379/// The latest protocol version supported by this implementation.
380///
381/// This is the preferred version that clients and servers will advertise during initialization.
382pub const PROTOCOL_VERSION: &str = "2025-11-25";
383
384/// All protocol versions supported by this implementation.
385///
386/// The SDK supports multiple protocol versions for backward compatibility:
387/// - `2025-11-25`: Latest version with tasks, parallel tools, agent loops
388/// - `2025-06-18`: Elicitation, structured output, resource links
389/// - `2025-03-26`: OAuth 2.1, Streamable HTTP, tool annotations, audio
390/// - `2024-11-05`: Original MCP specification, widely deployed
391///
392/// Version negotiation happens during initialization:
393/// 1. Client sends its preferred (latest) version
394/// 2. Server responds with the same version if supported, or its own preferred version
395/// 3. Client must support the server's version or disconnect
396///
397/// For type-safe version handling, use [`crate::protocol_version::ProtocolVersion`].
398///
399/// # Example
400///
401/// ```
402/// use mcpkit_core::capability::{SUPPORTED_PROTOCOL_VERSIONS, is_version_supported};
403///
404/// assert!(is_version_supported("2025-11-25"));
405/// assert!(is_version_supported("2025-06-18"));
406/// assert!(is_version_supported("2025-03-26"));
407/// assert!(is_version_supported("2024-11-05"));
408/// assert!(!is_version_supported("1.0.0"));
409/// ```
410pub const SUPPORTED_PROTOCOL_VERSIONS: &[&str] = &[
411    "2025-11-25", // Latest - tasks, parallel tools, agent loops
412    "2025-06-18", // Elicitation, structured output, resource links
413    "2025-03-26", // OAuth 2.1, Streamable HTTP, tool annotations
414    "2024-11-05", // Original MCP spec - widely deployed
415];
416
417/// Check if a protocol version is supported by this implementation.
418///
419/// # Arguments
420///
421/// * `version` - The protocol version string to check
422///
423/// # Returns
424///
425/// `true` if the version is in the list of supported versions, `false` otherwise.
426///
427/// # Example
428///
429/// ```
430/// use mcpkit_core::capability::is_version_supported;
431///
432/// assert!(is_version_supported("2025-11-25"));
433/// assert!(!is_version_supported("0.9.0"));
434/// ```
435#[must_use]
436pub fn is_version_supported(version: &str) -> bool {
437    SUPPORTED_PROTOCOL_VERSIONS.contains(&version)
438}
439
440/// Negotiate a protocol version between client and server.
441///
442/// Per the MCP specification:
443/// - If the requested version is supported, return it
444/// - Otherwise, return the server's preferred (latest) version
445///
446/// The client is then responsible for determining if it can support
447/// the returned version, and disconnecting if not.
448///
449/// # Arguments
450///
451/// * `requested_version` - The version requested by the client
452///
453/// # Returns
454///
455/// The negotiated protocol version string.
456///
457/// # Example
458///
459/// ```
460/// use mcpkit_core::capability::{negotiate_version, PROTOCOL_VERSION};
461///
462/// // Client requests a supported version - gets it back
463/// assert_eq!(negotiate_version("2024-11-05"), "2024-11-05");
464///
465/// // Client requests the latest version - gets it back
466/// assert_eq!(negotiate_version("2025-11-25"), "2025-11-25");
467///
468/// // Client requests unknown version - gets server's preferred version
469/// assert_eq!(negotiate_version("1.0.0"), PROTOCOL_VERSION);
470/// ```
471#[must_use]
472pub fn negotiate_version(requested_version: &str) -> &'static str {
473    if is_version_supported(requested_version) {
474        // Return the requested version if we support it
475        SUPPORTED_PROTOCOL_VERSIONS
476            .iter()
477            .find(|&&v| v == requested_version)
478            .copied()
479            .unwrap_or(PROTOCOL_VERSION)
480    } else {
481        // Return our preferred (latest) version
482        PROTOCOL_VERSION
483    }
484}
485
486/// Protocol version negotiation result.
487///
488/// Used internally to track the outcome of version negotiation.
489#[derive(Debug, Clone, PartialEq, Eq)]
490pub enum VersionNegotiationResult {
491    /// The requested version is supported and will be used.
492    Accepted(String),
493    /// The requested version is not supported; the server offers an alternative.
494    /// Client should check if it supports this alternative version.
495    CounterOffer {
496        /// The version requested by the client.
497        requested: String,
498        /// The version offered by the server.
499        offered: String,
500    },
501}
502
503impl VersionNegotiationResult {
504    /// Get the effective protocol version to use.
505    #[must_use]
506    pub fn version(&self) -> &str {
507        match self {
508            Self::Accepted(v) => v,
509            Self::CounterOffer { offered, .. } => offered,
510        }
511    }
512
513    /// Check if the negotiation was an exact match.
514    #[must_use]
515    pub const fn is_exact_match(&self) -> bool {
516        matches!(self, Self::Accepted(_))
517    }
518}
519
520/// Perform version negotiation and return detailed result.
521///
522/// This is useful when you need to know whether the negotiation
523/// resulted in an exact match or a counter-offer.
524///
525/// # Arguments
526///
527/// * `requested_version` - The version requested by the client
528///
529/// # Returns
530///
531/// A [`VersionNegotiationResult`] indicating whether the version was
532/// accepted or a counter-offer was made.
533///
534/// # Example
535///
536/// ```
537/// use mcpkit_core::capability::{negotiate_version_detailed, VersionNegotiationResult};
538///
539/// let result = negotiate_version_detailed("2024-11-05");
540/// assert!(result.is_exact_match());
541///
542/// let result = negotiate_version_detailed("unknown-version");
543/// assert!(!result.is_exact_match());
544/// ```
545#[must_use]
546pub fn negotiate_version_detailed(requested_version: &str) -> VersionNegotiationResult {
547    if is_version_supported(requested_version) {
548        VersionNegotiationResult::Accepted(requested_version.to_string())
549    } else {
550        VersionNegotiationResult::CounterOffer {
551            requested: requested_version.to_string(),
552            offered: PROTOCOL_VERSION.to_string(),
553        }
554    }
555}
556
557/// Initialized notification (sent by client after receiving initialize result).
558#[derive(Debug, Clone, Default, Serialize, Deserialize)]
559pub struct InitializedNotification {}
560
561/// Ping request for keep-alive.
562#[derive(Debug, Clone, Default, Serialize, Deserialize)]
563pub struct PingRequest {}
564
565/// Ping response.
566#[derive(Debug, Clone, Default, Serialize, Deserialize)]
567pub struct PingResult {}
568
569#[cfg(test)]
570mod tests {
571    use super::*;
572
573    #[test]
574    fn test_server_capabilities_builder() {
575        let caps = ServerCapabilities::new()
576            .with_tools()
577            .with_resources_and_subscriptions()
578            .with_prompts()
579            .with_tasks();
580
581        assert!(caps.has_tools());
582        assert!(caps.has_resources());
583        assert!(caps.has_prompts());
584        assert!(caps.has_tasks());
585        assert!(caps.resources.unwrap().subscribe.unwrap());
586    }
587
588    #[test]
589    fn test_client_capabilities_builder() {
590        let caps = ClientCapabilities::new()
591            .with_roots_and_changes()
592            .with_sampling()
593            .with_elicitation();
594
595        assert!(caps.has_roots());
596        assert!(caps.has_sampling());
597        assert!(caps.has_elicitation());
598        assert!(caps.roots.unwrap().list_changed.unwrap());
599    }
600
601    #[test]
602    fn test_initialize_request() {
603        let client = ClientInfo::new("test-client", "1.0.0");
604        let caps = ClientCapabilities::new().with_sampling();
605        let request = InitializeRequest::new(client, caps);
606
607        assert_eq!(request.protocol_version, PROTOCOL_VERSION);
608        assert_eq!(request.client_info.name, "test-client");
609    }
610
611    #[test]
612    fn test_initialize_result() {
613        let server = ServerInfo::new("test-server", "1.0.0");
614        let caps = ServerCapabilities::new().with_tools();
615        let result =
616            InitializeResult::new(server, caps).instructions("Use this server to do things");
617
618        assert_eq!(result.protocol_version, PROTOCOL_VERSION);
619        assert!(result.instructions.is_some());
620    }
621
622    #[test]
623    fn test_serialization() {
624        let caps = ServerCapabilities::new()
625            .with_tools_and_changes()
626            .with_resources();
627
628        let json = serde_json::to_string(&caps).unwrap();
629        assert!(json.contains("\"tools\""));
630        assert!(json.contains("\"listChanged\":true"));
631    }
632}