Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions crates/rmcp/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -889,6 +889,15 @@ impl Default for ClientInfo {
}
}

/// Icon themes supported by the MCP specification
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Copy)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding these would let consumers use it as a HashMap key or in HashSet without friction.

Suggested change
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Copy)]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash, Copy)]

#[serde(rename_all = "lowercase")] //match spec
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum IconTheme {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The MCP spec could add more themes in the future (e.g., high-contrast). This would prevent downstream match arms from breaking when new variants are added.

Suggested change
pub enum IconTheme {
#[non_exhaustive]
pub enum IconTheme {

Light,
Dark,
}

/// A URL pointing to an icon resource or a base64-encoded data URI.
///
/// Clients that support rendering icons MUST support at least the following MIME types:
Expand All @@ -911,6 +920,9 @@ pub struct Icon {
/// Size specification, each string should be in WxH format (e.g., `\"48x48\"`, `\"96x96\"`) or `\"any\"` for scalable formats like SVG
#[serde(skip_serializing_if = "Option::is_none")]
pub sizes: Option<Vec<String>>,
/// Optional specifier for the theme this icon is designed for
#[serde(skip_serializing_if = "Option::is_none")]
pub theme: Option<IconTheme>,
}

impl Icon {
Expand All @@ -920,6 +932,7 @@ impl Icon {
src: src.into(),
mime_type: None,
sizes: None,
theme: None,
}
}

Expand All @@ -934,6 +947,12 @@ impl Icon {
self.sizes = Some(sizes);
self
}

/// Set the theme.
pub fn with_theme(mut self, theme: IconTheme) -> Self {
self.theme = Some(theme);
self
}
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
Expand Down Expand Up @@ -3642,12 +3661,14 @@ mod tests {
src: "https://example.com/icon.png".to_string(),
mime_type: Some("image/png".to_string()),
sizes: Some(vec!["48x48".to_string()]),
theme: Some(IconTheme::Light),
};

let json = serde_json::to_value(&icon).unwrap();
assert_eq!(json["src"], "https://example.com/icon.png");
assert_eq!(json["mimeType"], "image/png");
assert_eq!(json["sizes"][0], "48x48");
assert_eq!(json["theme"], "light");

// Test deserialization
let deserialized: Icon = serde_json::from_value(json).unwrap();
Expand All @@ -3660,12 +3681,14 @@ mod tests {
src: "data:image/svg+xml;base64,PHN2Zy8+".to_string(),
mime_type: None,
sizes: None,
theme: None,
};

let json = serde_json::to_value(&icon).unwrap();
assert_eq!(json["src"], "data:image/svg+xml;base64,PHN2Zy8+");
assert!(json.get("mimeType").is_none());
assert!(json.get("sizes").is_none());
assert!(json.get("theme").is_none());
}

#[test]
Expand All @@ -3680,11 +3703,13 @@ mod tests {
src: "https://example.com/icon.png".to_string(),
mime_type: Some("image/png".to_string()),
sizes: Some(vec!["48x48".to_string()]),
theme: Some(IconTheme::Dark),
},
Icon {
src: "https://example.com/icon.svg".to_string(),
mime_type: Some("image/svg+xml".to_string()),
sizes: Some(vec!["any".to_string()]),
theme: Some(IconTheme::Light),
},
]),
website_url: Some("https://example.com".to_string()),
Expand All @@ -3699,6 +3724,8 @@ mod tests {
assert_eq!(json["icons"][0]["sizes"][0], "48x48");
assert_eq!(json["icons"][1]["mimeType"], "image/svg+xml");
assert_eq!(json["icons"][1]["sizes"][0], "any");
assert_eq!(json["icons"][0]["theme"], "dark");
assert_eq!(json["icons"][1]["theme"], "light");
}

#[test]
Expand Down Expand Up @@ -3731,6 +3758,7 @@ mod tests {
src: "https://example.com/server.png".to_string(),
mime_type: Some("image/png".to_string()),
sizes: Some(vec!["48x48".to_string()]),
theme: Some(IconTheme::Light),
}]),
website_url: Some("https://docs.example.com".to_string()),
},
Expand All @@ -3744,6 +3772,7 @@ mod tests {
"https://example.com/server.png"
);
assert_eq!(json["serverInfo"]["icons"][0]["sizes"][0], "48x48");
assert_eq!(json["serverInfo"]["icons"][0]["theme"], "light");
assert_eq!(json["serverInfo"]["websiteUrl"], "https://docs.example.com");
}

Expand Down
3 changes: 3 additions & 0 deletions crates/rmcp/src/model/resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ mod tests {
use serde_json;

use super::*;
use crate::model::IconTheme;

#[test]
fn test_resource_serialization() {
Expand Down Expand Up @@ -265,13 +266,15 @@ mod tests {
src: "https://example.com/icon.png".to_string(),
mime_type: Some("image/png".to_string()),
sizes: Some(vec!["48x48".to_string()]),
theme: Some(IconTheme::Light),
}]),
};

let json = serde_json::to_value(&resource_template).unwrap();
assert!(json["icons"].is_array());
assert_eq!(json["icons"][0]["src"], "https://example.com/icon.png");
assert_eq!(json["icons"][0]["sizes"][0], "48x48");
assert_eq!(json["icons"][0]["theme"], "light");
}

#[test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -720,12 +720,31 @@
"src": {
"description": "A standard URI pointing to an icon resource",
"type": "string"
},
"theme": {
"description": "Optional specifier for the theme this icon is designed for",
"anyOf": [
{
"$ref": "#/definitions/IconTheme"
},
{
"type": "null"
}
]
}
},
"required": [
"src"
]
},
"IconTheme": {
"description": "Icon themes supported by the MCP specification",
"type": "string",
"enum": [
"light",
"dark"
]
},
"Implementation": {
"type": "object",
"properties": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -720,12 +720,31 @@
"src": {
"description": "A standard URI pointing to an icon resource",
"type": "string"
},
"theme": {
"description": "Optional specifier for the theme this icon is designed for",
"anyOf": [
{
"$ref": "#/definitions/IconTheme"
},
{
"type": "null"
}
]
}
},
"required": [
"src"
]
},
"IconTheme": {
"description": "Icon themes supported by the MCP specification",
"type": "string",
"enum": [
"light",
"dark"
]
},
"Implementation": {
"type": "object",
"properties": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1090,12 +1090,31 @@
"src": {
"description": "A standard URI pointing to an icon resource",
"type": "string"
},
"theme": {
"description": "Optional specifier for the theme this icon is designed for",
"anyOf": [
{
"$ref": "#/definitions/IconTheme"
},
{
"type": "null"
}
]
}
},
"required": [
"src"
]
},
"IconTheme": {
"description": "Icon themes supported by the MCP specification",
"type": "string",
"enum": [
"light",
"dark"
]
},
"Implementation": {
"type": "object",
"properties": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1090,12 +1090,31 @@
"src": {
"description": "A standard URI pointing to an icon resource",
"type": "string"
},
"theme": {
"description": "Optional specifier for the theme this icon is designed for",
"anyOf": [
{
"$ref": "#/definitions/IconTheme"
},
{
"type": "null"
}
]
}
},
"required": [
"src"
]
},
"IconTheme": {
"description": "Icon themes supported by the MCP specification",
"type": "string",
"enum": [
"light",
"dark"
]
},
"Implementation": {
"type": "object",
"properties": {
Expand Down
Loading