Skip to content

Commit 0b36a84

Browse files
glichtDaleSeo
andauthored
feat: add "theme" to Icon (#766)
* feat: add theme field to Icon * fix: update IconThem crates/rmcp/src/model.rs (non_exhaustive) Co-authored-by: Dale Seo <[email protected]> * fix: update IconThem crates/rmcp/src/model.rs (eq, hash) Co-authored-by: Dale Seo <[email protected]> * fix: update docs with full descriptions of theme from mcp spec --------- Co-authored-by: Dale Seo <[email protected]>
1 parent 6a3b32d commit 0b36a84

File tree

6 files changed

+140
-0
lines changed

6 files changed

+140
-0
lines changed

crates/rmcp/src/model.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -907,6 +907,18 @@ impl Default for ClientInfo {
907907
}
908908
}
909909

910+
/// Icon themes supported by the MCP specification
911+
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash, Copy)]
912+
#[serde(rename_all = "lowercase")] //match spec
913+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
914+
#[non_exhaustive]
915+
pub enum IconTheme {
916+
/// Indicates the icon is designed to be used with a light background
917+
Light,
918+
/// Indicates the icon is designed to be used with a dark background
919+
Dark,
920+
}
921+
910922
/// A URL pointing to an icon resource or a base64-encoded data URI.
911923
///
912924
/// Clients that support rendering icons MUST support at least the following MIME types:
@@ -929,6 +941,10 @@ pub struct Icon {
929941
/// Size specification, each string should be in WxH format (e.g., `\"48x48\"`, `\"96x96\"`) or `\"any\"` for scalable formats like SVG
930942
#[serde(skip_serializing_if = "Option::is_none")]
931943
pub sizes: Option<Vec<String>>,
944+
/// Optional specifier for the theme this icon is designed for
945+
/// If not provided, the client should assume the icon can be used with any theme.
946+
#[serde(skip_serializing_if = "Option::is_none")]
947+
pub theme: Option<IconTheme>,
932948
}
933949

934950
impl Icon {
@@ -938,6 +954,7 @@ impl Icon {
938954
src: src.into(),
939955
mime_type: None,
940956
sizes: None,
957+
theme: None,
941958
}
942959
}
943960

@@ -952,6 +969,12 @@ impl Icon {
952969
self.sizes = Some(sizes);
953970
self
954971
}
972+
973+
/// Set the theme.
974+
pub fn with_theme(mut self, theme: IconTheme) -> Self {
975+
self.theme = Some(theme);
976+
self
977+
}
955978
}
956979

957980
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
@@ -3725,12 +3748,14 @@ mod tests {
37253748
src: "https://example.com/icon.png".to_string(),
37263749
mime_type: Some("image/png".to_string()),
37273750
sizes: Some(vec!["48x48".to_string()]),
3751+
theme: Some(IconTheme::Light),
37283752
};
37293753

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

37353760
// Test deserialization
37363761
let deserialized: Icon = serde_json::from_value(json).unwrap();
@@ -3743,12 +3768,14 @@ mod tests {
37433768
src: "data:image/svg+xml;base64,PHN2Zy8+".to_string(),
37443769
mime_type: None,
37453770
sizes: None,
3771+
theme: None,
37463772
};
37473773

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

37543781
#[test]
@@ -3763,11 +3790,13 @@ mod tests {
37633790
src: "https://example.com/icon.png".to_string(),
37643791
mime_type: Some("image/png".to_string()),
37653792
sizes: Some(vec!["48x48".to_string()]),
3793+
theme: Some(IconTheme::Dark),
37663794
},
37673795
Icon {
37683796
src: "https://example.com/icon.svg".to_string(),
37693797
mime_type: Some("image/svg+xml".to_string()),
37703798
sizes: Some(vec!["any".to_string()]),
3799+
theme: Some(IconTheme::Light),
37713800
},
37723801
]),
37733802
website_url: Some("https://example.com".to_string()),
@@ -3782,6 +3811,8 @@ mod tests {
37823811
assert_eq!(json["icons"][0]["sizes"][0], "48x48");
37833812
assert_eq!(json["icons"][1]["mimeType"], "image/svg+xml");
37843813
assert_eq!(json["icons"][1]["sizes"][0], "any");
3814+
assert_eq!(json["icons"][0]["theme"], "dark");
3815+
assert_eq!(json["icons"][1]["theme"], "light");
37853816
}
37863817

37873818
#[test]
@@ -3814,6 +3845,7 @@ mod tests {
38143845
src: "https://example.com/server.png".to_string(),
38153846
mime_type: Some("image/png".to_string()),
38163847
sizes: Some(vec!["48x48".to_string()]),
3848+
theme: Some(IconTheme::Light),
38173849
}]),
38183850
website_url: Some("https://docs.example.com".to_string()),
38193851
},
@@ -3827,6 +3859,7 @@ mod tests {
38273859
"https://example.com/server.png"
38283860
);
38293861
assert_eq!(json["serverInfo"]["icons"][0]["sizes"][0], "48x48");
3862+
assert_eq!(json["serverInfo"]["icons"][0]["theme"], "light");
38303863
assert_eq!(json["serverInfo"]["websiteUrl"], "https://docs.example.com");
38313864
}
38323865

crates/rmcp/src/model/resource.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ mod tests {
217217
use serde_json;
218218

219219
use super::*;
220+
use crate::model::IconTheme;
220221

221222
#[test]
222223
fn test_resource_serialization() {
@@ -268,13 +269,15 @@ mod tests {
268269
src: "https://example.com/icon.png".to_string(),
269270
mime_type: Some("image/png".to_string()),
270271
sizes: Some(vec!["48x48".to_string()]),
272+
theme: Some(IconTheme::Light),
271273
}]),
272274
};
273275

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

280283
#[test]

crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -720,12 +720,38 @@
720720
"src": {
721721
"description": "A standard URI pointing to an icon resource",
722722
"type": "string"
723+
},
724+
"theme": {
725+
"description": "Optional specifier for the theme this icon is designed for\nIf not provided, the client should assume the icon can be used with any theme.",
726+
"anyOf": [
727+
{
728+
"$ref": "#/definitions/IconTheme"
729+
},
730+
{
731+
"type": "null"
732+
}
733+
]
723734
}
724735
},
725736
"required": [
726737
"src"
727738
]
728739
},
740+
"IconTheme": {
741+
"description": "Icon themes supported by the MCP specification",
742+
"oneOf": [
743+
{
744+
"description": "Indicates the icon is designed to be used with a light background",
745+
"type": "string",
746+
"const": "light"
747+
},
748+
{
749+
"description": "Indicates the icon is designed to be used with a dark background",
750+
"type": "string",
751+
"const": "dark"
752+
}
753+
]
754+
},
729755
"Implementation": {
730756
"type": "object",
731757
"properties": {

crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema_current.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -720,12 +720,38 @@
720720
"src": {
721721
"description": "A standard URI pointing to an icon resource",
722722
"type": "string"
723+
},
724+
"theme": {
725+
"description": "Optional specifier for the theme this icon is designed for\nIf not provided, the client should assume the icon can be used with any theme.",
726+
"anyOf": [
727+
{
728+
"$ref": "#/definitions/IconTheme"
729+
},
730+
{
731+
"type": "null"
732+
}
733+
]
723734
}
724735
},
725736
"required": [
726737
"src"
727738
]
728739
},
740+
"IconTheme": {
741+
"description": "Icon themes supported by the MCP specification",
742+
"oneOf": [
743+
{
744+
"description": "Indicates the icon is designed to be used with a light background",
745+
"type": "string",
746+
"const": "light"
747+
},
748+
{
749+
"description": "Indicates the icon is designed to be used with a dark background",
750+
"type": "string",
751+
"const": "dark"
752+
}
753+
]
754+
},
729755
"Implementation": {
730756
"type": "object",
731757
"properties": {

crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,12 +1090,38 @@
10901090
"src": {
10911091
"description": "A standard URI pointing to an icon resource",
10921092
"type": "string"
1093+
},
1094+
"theme": {
1095+
"description": "Optional specifier for the theme this icon is designed for\nIf not provided, the client should assume the icon can be used with any theme.",
1096+
"anyOf": [
1097+
{
1098+
"$ref": "#/definitions/IconTheme"
1099+
},
1100+
{
1101+
"type": "null"
1102+
}
1103+
]
10931104
}
10941105
},
10951106
"required": [
10961107
"src"
10971108
]
10981109
},
1110+
"IconTheme": {
1111+
"description": "Icon themes supported by the MCP specification",
1112+
"oneOf": [
1113+
{
1114+
"description": "Indicates the icon is designed to be used with a light background",
1115+
"type": "string",
1116+
"const": "light"
1117+
},
1118+
{
1119+
"description": "Indicates the icon is designed to be used with a dark background",
1120+
"type": "string",
1121+
"const": "dark"
1122+
}
1123+
]
1124+
},
10991125
"Implementation": {
11001126
"type": "object",
11011127
"properties": {

crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,12 +1092,38 @@
10921092
"src": {
10931093
"description": "A standard URI pointing to an icon resource",
10941094
"type": "string"
1095+
},
1096+
"theme": {
1097+
"description": "Optional specifier for the theme this icon is designed for\nIf not provided, the client should assume the icon can be used with any theme.",
1098+
"anyOf": [
1099+
{
1100+
"$ref": "#/definitions/IconTheme"
1101+
},
1102+
{
1103+
"type": "null"
1104+
}
1105+
]
10951106
}
10961107
},
10971108
"required": [
10981109
"src"
10991110
]
11001111
},
1112+
"IconTheme": {
1113+
"description": "Icon themes supported by the MCP specification",
1114+
"oneOf": [
1115+
{
1116+
"description": "Indicates the icon is designed to be used with a light background",
1117+
"type": "string",
1118+
"const": "light"
1119+
},
1120+
{
1121+
"description": "Indicates the icon is designed to be used with a dark background",
1122+
"type": "string",
1123+
"const": "dark"
1124+
}
1125+
]
1126+
},
11011127
"Implementation": {
11021128
"type": "object",
11031129
"properties": {

0 commit comments

Comments
 (0)