Skip to main content

turbomcp_types/
primitives.rs

1//! Primitive wire-format types shared across the MCP protocol.
2//!
3//! These are the domain newtypes and forward-compatible version enum used
4//! throughout MCP messages. They are `no_std + alloc`-compatible.
5//!
6//! ## Inventory
7//!
8//! - [`ProtocolVersion`] — MCP spec version enum with forward-compatible
9//!   [`Unknown`](ProtocolVersion::Unknown) variant for unrecognised strings.
10//! - [`Uri`] — transparent `String` newtype for URIs.
11//! - [`MimeType`] — transparent `String` newtype for MIME types.
12//! - [`Base64String`] — transparent `String` newtype for base64-encoded data.
13//! - [`BaseMetadata`] — `{ name, title }` used as a base for many MCP entities.
14//! - [`Cursor`] — pagination cursor alias (`String`).
15
16use core::fmt;
17use core::ops::Deref;
18
19#[cfg(not(feature = "std"))]
20use alloc::string::String;
21
22use serde::{Deserialize, Deserializer, Serialize, Serializer};
23
24// =============================================================================
25// ProtocolVersion
26// =============================================================================
27
28/// MCP protocol version.
29///
30/// Represents a known or unknown MCP specification version. Known versions get
31/// first-class enum variants; unknown version strings are preserved via
32/// [`Unknown`](ProtocolVersion::Unknown) for forward compatibility (e.g. proxies
33/// and protocol analyzers that handle arbitrary versions).
34///
35/// ## Ordering
36///
37/// Known versions are ordered by specification release date.
38/// [`Unknown`](ProtocolVersion::Unknown) sorts after all known versions.
39///
40/// ## Serialization
41///
42/// Serializes to/from the canonical version string (e.g. `"2025-11-25"`).
43#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
44pub enum ProtocolVersion {
45    /// MCP specification 2025-06-18.
46    V2025_06_18,
47    /// MCP specification 2025-11-25 (current stable).
48    #[default]
49    V2025_11_25,
50    /// Draft specification (`DRAFT-2026-v1`).
51    Draft,
52    /// Unknown / future protocol version (preserved for forward compatibility).
53    Unknown(String),
54}
55
56impl ProtocolVersion {
57    /// The latest stable protocol version.
58    pub const LATEST: Self = Self::V2025_11_25;
59
60    /// All stable (released) protocol versions, oldest to newest.
61    ///
62    /// Does not include [`Draft`](Self::Draft) or [`Unknown`](Self::Unknown).
63    pub const STABLE: &[Self] = &[Self::V2025_06_18, Self::V2025_11_25];
64
65    /// The canonical version string for this protocol version.
66    #[must_use]
67    pub fn as_str(&self) -> &str {
68        match self {
69            Self::V2025_06_18 => "2025-06-18",
70            Self::V2025_11_25 => "2025-11-25",
71            Self::Draft => "DRAFT-2026-v1",
72            Self::Unknown(s) => s.as_str(),
73        }
74    }
75
76    /// Whether this is a named (non-`Unknown`) protocol version.
77    #[must_use]
78    pub fn is_known(&self) -> bool {
79        !matches!(self, Self::Unknown(_))
80    }
81
82    /// Whether this is a stable (released) protocol version.
83    ///
84    /// Returns `false` for [`Draft`](Self::Draft) and [`Unknown`](Self::Unknown).
85    #[must_use]
86    pub fn is_stable(&self) -> bool {
87        matches!(self, Self::V2025_06_18 | Self::V2025_11_25)
88    }
89
90    /// Whether this is the named draft specification (`DRAFT-2026-v1`).
91    ///
92    /// Note: only the named `DRAFT-2026-v1` enum variant returns `true`. Future or other
93    /// draft strings (e.g. `DRAFT-2026-v2`) are routed to `Unknown(_)` by [`From<&str>`]
94    /// and will not match. Use [`Self::is_any_draft`] to detect any string starting with
95    /// `DRAFT-`.
96    #[must_use]
97    pub fn is_draft(&self) -> bool {
98        matches!(self, Self::Draft)
99    }
100
101    /// Whether this version is any draft — the named `DRAFT-2026-v1` variant or
102    /// an `Unknown(s)` whose string starts with `DRAFT-`.
103    #[must_use]
104    pub fn is_any_draft(&self) -> bool {
105        match self {
106            Self::Draft => true,
107            Self::Unknown(s) => s.starts_with("DRAFT-"),
108            _ => false,
109        }
110    }
111
112    fn ordinal(&self) -> u32 {
113        match self {
114            Self::V2025_06_18 => 1,
115            Self::V2025_11_25 => 2,
116            Self::Draft => 3,
117            Self::Unknown(_) => u32::MAX,
118        }
119    }
120}
121
122impl fmt::Display for ProtocolVersion {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        f.write_str(self.as_str())
125    }
126}
127
128impl PartialOrd for ProtocolVersion {
129    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
130        Some(self.cmp(other))
131    }
132}
133
134impl Ord for ProtocolVersion {
135    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
136        match (self, other) {
137            (Self::Unknown(a), Self::Unknown(b)) => a.cmp(b),
138            _ => self.ordinal().cmp(&other.ordinal()),
139        }
140    }
141}
142
143impl From<&str> for ProtocolVersion {
144    fn from(s: &str) -> Self {
145        match s {
146            "2025-06-18" => Self::V2025_06_18,
147            "2025-11-25" => Self::V2025_11_25,
148            "DRAFT-2026-v1" => Self::Draft,
149            other => Self::Unknown(other.into()),
150        }
151    }
152}
153
154impl From<String> for ProtocolVersion {
155    fn from(s: String) -> Self {
156        match s.as_str() {
157            "2025-06-18" => Self::V2025_06_18,
158            "2025-11-25" => Self::V2025_11_25,
159            "DRAFT-2026-v1" => Self::Draft,
160            _ => Self::Unknown(s),
161        }
162    }
163}
164
165impl From<ProtocolVersion> for String {
166    fn from(v: ProtocolVersion) -> Self {
167        match v {
168            ProtocolVersion::Unknown(s) => s,
169            other => other.as_str().into(),
170        }
171    }
172}
173
174impl Serialize for ProtocolVersion {
175    fn serialize<S: Serializer>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error> {
176        serializer.serialize_str(self.as_str())
177    }
178}
179
180impl<'de> Deserialize<'de> for ProtocolVersion {
181    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> core::result::Result<Self, D::Error> {
182        let s = String::deserialize(deserializer)?;
183        Ok(ProtocolVersion::from(s))
184    }
185}
186
187impl PartialEq<&str> for ProtocolVersion {
188    fn eq(&self, other: &&str) -> bool {
189        self.as_str() == *other
190    }
191}
192
193impl PartialEq<ProtocolVersion> for &str {
194    fn eq(&self, other: &ProtocolVersion) -> bool {
195        *self == other.as_str()
196    }
197}
198
199// =============================================================================
200// Uri
201// =============================================================================
202
203/// Transparent `String` newtype for URIs.
204///
205/// No validation is performed on construction. If callers need to verify the
206/// URI parses cleanly, they can apply their own check (for example
207/// `url::Url::parse(uri.as_str())` from the `url` crate).
208#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
209#[serde(transparent)]
210pub struct Uri(String);
211
212impl Uri {
213    /// Create a URI wrapper without additional validation.
214    #[must_use]
215    pub fn new(uri: impl Into<String>) -> Self {
216        Self(uri.into())
217    }
218
219    /// Borrow the inner string.
220    #[must_use]
221    pub fn as_str(&self) -> &str {
222        &self.0
223    }
224
225    /// Consume into the underlying string.
226    #[must_use]
227    pub fn into_inner(self) -> String {
228        self.0
229    }
230}
231
232impl fmt::Display for Uri {
233    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234        self.0.fmt(f)
235    }
236}
237
238impl Deref for Uri {
239    type Target = str;
240
241    fn deref(&self) -> &Self::Target {
242        &self.0
243    }
244}
245
246impl AsRef<str> for Uri {
247    fn as_ref(&self) -> &str {
248        &self.0
249    }
250}
251
252impl From<String> for Uri {
253    fn from(value: String) -> Self {
254        Self(value)
255    }
256}
257
258impl From<&str> for Uri {
259    fn from(value: &str) -> Self {
260        Self(value.into())
261    }
262}
263
264impl From<Uri> for String {
265    fn from(value: Uri) -> Self {
266        value.0
267    }
268}
269
270impl PartialEq<&str> for Uri {
271    fn eq(&self, other: &&str) -> bool {
272        self.as_str() == *other
273    }
274}
275
276impl PartialEq<Uri> for &str {
277    fn eq(&self, other: &Uri) -> bool {
278        *self == other.as_str()
279    }
280}
281
282// =============================================================================
283// MimeType
284// =============================================================================
285
286/// Transparent `String` newtype for MIME types.
287#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
288#[serde(transparent)]
289pub struct MimeType(String);
290
291impl MimeType {
292    /// Create a MIME type wrapper without additional validation.
293    #[must_use]
294    pub fn new(mime_type: impl Into<String>) -> Self {
295        Self(mime_type.into())
296    }
297
298    /// Borrow the inner string.
299    #[must_use]
300    pub fn as_str(&self) -> &str {
301        &self.0
302    }
303}
304
305impl fmt::Display for MimeType {
306    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
307        self.0.fmt(f)
308    }
309}
310
311impl Deref for MimeType {
312    type Target = str;
313
314    fn deref(&self) -> &Self::Target {
315        &self.0
316    }
317}
318
319impl AsRef<str> for MimeType {
320    fn as_ref(&self) -> &str {
321        &self.0
322    }
323}
324
325impl From<String> for MimeType {
326    fn from(value: String) -> Self {
327        Self(value)
328    }
329}
330
331impl From<&str> for MimeType {
332    fn from(value: &str) -> Self {
333        Self(value.into())
334    }
335}
336
337impl From<MimeType> for String {
338    fn from(value: MimeType) -> Self {
339        value.0
340    }
341}
342
343impl PartialEq<&str> for MimeType {
344    fn eq(&self, other: &&str) -> bool {
345        self.as_str() == *other
346    }
347}
348
349impl PartialEq<MimeType> for &str {
350    fn eq(&self, other: &MimeType) -> bool {
351        *self == other.as_str()
352    }
353}
354
355// =============================================================================
356// Base64String
357// =============================================================================
358
359/// Transparent `String` newtype for base64-encoded payloads.
360#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
361#[serde(transparent)]
362pub struct Base64String(String);
363
364impl Base64String {
365    /// Create a base64 wrapper without additional validation.
366    #[must_use]
367    pub fn new(data: impl Into<String>) -> Self {
368        Self(data.into())
369    }
370
371    /// Borrow the inner string.
372    #[must_use]
373    pub fn as_str(&self) -> &str {
374        &self.0
375    }
376}
377
378impl fmt::Display for Base64String {
379    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
380        self.0.fmt(f)
381    }
382}
383
384impl Deref for Base64String {
385    type Target = str;
386
387    fn deref(&self) -> &Self::Target {
388        &self.0
389    }
390}
391
392impl AsRef<str> for Base64String {
393    fn as_ref(&self) -> &str {
394        &self.0
395    }
396}
397
398impl From<String> for Base64String {
399    fn from(value: String) -> Self {
400        Self(value)
401    }
402}
403
404impl From<&str> for Base64String {
405    fn from(value: &str) -> Self {
406        Self(value.into())
407    }
408}
409
410impl From<Base64String> for String {
411    fn from(value: Base64String) -> Self {
412        value.0
413    }
414}
415
416impl PartialEq<&str> for Base64String {
417    fn eq(&self, other: &&str) -> bool {
418        self.as_str() == *other
419    }
420}
421
422impl PartialEq<Base64String> for &str {
423    fn eq(&self, other: &Base64String) -> bool {
424        *self == other.as_str()
425    }
426}
427
428// =============================================================================
429// BaseMetadata
430// =============================================================================
431
432/// Base metadata shared by MCP entities that carry a programmatic name and an
433/// optional human-readable title.
434#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
435pub struct BaseMetadata {
436    /// Programmatic name / identifier.
437    pub name: String,
438    /// Human-readable display title.
439    #[serde(skip_serializing_if = "Option::is_none")]
440    pub title: Option<String>,
441}
442
443impl BaseMetadata {
444    /// Create metadata with a name and no title.
445    #[must_use]
446    pub fn new(name: impl Into<String>) -> Self {
447        Self {
448            name: name.into(),
449            title: None,
450        }
451    }
452
453    /// Set the display title.
454    #[must_use]
455    pub fn with_title(mut self, title: impl Into<String>) -> Self {
456        self.title = Some(title.into());
457        self
458    }
459}
460
461// =============================================================================
462// Cursor
463// =============================================================================
464
465/// Pagination cursor (opaque string per MCP 2025-11-25).
466pub type Cursor = String;
467
468#[cfg(test)]
469mod tests {
470    use super::*;
471
472    #[test]
473    fn protocol_version_serde_roundtrip() {
474        let v = ProtocolVersion::V2025_11_25;
475        let s = serde_json::to_string(&v).unwrap();
476        assert_eq!(s, "\"2025-11-25\"");
477        let back: ProtocolVersion = serde_json::from_str(&s).unwrap();
478        assert_eq!(back, v);
479    }
480
481    #[test]
482    fn protocol_version_unknown_preserves_string() {
483        let v: ProtocolVersion = "future-version".into();
484        assert_eq!(v, ProtocolVersion::Unknown("future-version".into()));
485        assert!(!v.is_known());
486        assert!(!v.is_stable());
487    }
488
489    #[test]
490    fn protocol_version_ordering() {
491        assert!(ProtocolVersion::V2025_06_18 < ProtocolVersion::V2025_11_25);
492        assert!(ProtocolVersion::V2025_11_25 < ProtocolVersion::Draft);
493        assert!(ProtocolVersion::Draft < ProtocolVersion::Unknown("x".into()));
494    }
495
496    #[test]
497    fn uri_transparent_serde() {
498        let uri = Uri::new("file:///path/to/file.txt");
499        let s = serde_json::to_string(&uri).unwrap();
500        assert_eq!(s, "\"file:///path/to/file.txt\"");
501        let back: Uri = serde_json::from_str(&s).unwrap();
502        assert_eq!(back, uri);
503    }
504
505    #[test]
506    fn mime_type_and_base64_roundtrips() {
507        let m = MimeType::new("application/json");
508        assert_eq!(m.as_str(), "application/json");
509        let b = Base64String::new("aGVsbG8=");
510        assert_eq!(b.as_str(), "aGVsbG8=");
511    }
512
513    #[test]
514    fn base_metadata_builder() {
515        let meta = BaseMetadata::new("my_tool").with_title("My Tool");
516        assert_eq!(meta.name, "my_tool");
517        assert_eq!(meta.title.as_deref(), Some("My Tool"));
518        let json = serde_json::to_string(&meta).unwrap();
519        assert!(json.contains("\"name\":\"my_tool\""));
520        assert!(json.contains("\"title\":\"My Tool\""));
521    }
522
523    #[test]
524    fn base_metadata_omits_absent_title() {
525        let meta = BaseMetadata::new("x");
526        let json = serde_json::to_string(&meta).unwrap();
527        assert!(!json.contains("title"));
528    }
529}