feat(SEP-973): add support for icons and websiteUrl across relevant types (#432)
This commit is contained in:
parent
6fbd941a32
commit
b57aa47693
5 changed files with 611 additions and 63 deletions
|
|
@ -693,13 +693,41 @@ impl Default for ClientInfo {
|
|||
}
|
||||
}
|
||||
|
||||
/// 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:
|
||||
/// - image/png - PNG images (safe, universal compatibility)
|
||||
/// - image/jpeg (and image/jpg) - JPEG images (safe, universal compatibility)
|
||||
///
|
||||
/// Clients that support rendering icons SHOULD also support:
|
||||
/// - image/svg+xml - SVG images (scalable but requires security precautions)
|
||||
/// - image/webp - WebP images (modern, efficient format)
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct Icon {
|
||||
/// A standard URI pointing to an icon resource
|
||||
pub src: String,
|
||||
/// Optional override if the server's MIME type is missing or generic
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mime_type: Option<String>,
|
||||
/// Size specification (e.g., "48x48", "any" for SVG, or "48x48 96x96")
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sizes: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct Implementation {
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
pub version: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub icons: Option<Vec<Icon>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub website_url: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for Implementation {
|
||||
|
|
@ -714,6 +742,8 @@ impl Implementation {
|
|||
name: env!("CARGO_CRATE_NAME").to_owned(),
|
||||
title: None,
|
||||
version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||
icons: None,
|
||||
website_url: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1926,6 +1956,7 @@ mod tests {
|
|||
assert_eq!(capabilities.tools.unwrap().list_changed, Some(true));
|
||||
assert_eq!(server_info.name, "ExampleServer");
|
||||
assert_eq!(server_info.version, "1.0.0");
|
||||
assert_eq!(server_info.icons, None);
|
||||
assert_eq!(instructions, None);
|
||||
}
|
||||
other => panic!("Expected InitializeResult, got {other:?}"),
|
||||
|
|
@ -2021,4 +2052,108 @@ mod tests {
|
|||
let v2 = ProtocolVersion::V_2025_03_26;
|
||||
assert!(v1 < v2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_icon_serialization() {
|
||||
let icon = Icon {
|
||||
src: "https://example.com/icon.png".to_string(),
|
||||
mime_type: Some("image/png".to_string()),
|
||||
sizes: Some("48x48".to_string()),
|
||||
};
|
||||
|
||||
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"], "48x48");
|
||||
|
||||
// Test deserialization
|
||||
let deserialized: Icon = serde_json::from_value(json).unwrap();
|
||||
assert_eq!(deserialized, icon);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_icon_minimal() {
|
||||
let icon = Icon {
|
||||
src: "data:image/svg+xml;base64,PHN2Zy8+".to_string(),
|
||||
mime_type: None,
|
||||
sizes: 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());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_implementation_with_icons() {
|
||||
let implementation = Implementation {
|
||||
name: "test-server".to_string(),
|
||||
title: Some("Test Server".to_string()),
|
||||
version: "1.0.0".to_string(),
|
||||
icons: Some(vec![
|
||||
Icon {
|
||||
src: "https://example.com/icon.png".to_string(),
|
||||
mime_type: Some("image/png".to_string()),
|
||||
sizes: Some("48x48".to_string()),
|
||||
},
|
||||
Icon {
|
||||
src: "https://example.com/icon.svg".to_string(),
|
||||
mime_type: Some("image/svg+xml".to_string()),
|
||||
sizes: Some("any".to_string()),
|
||||
},
|
||||
]),
|
||||
website_url: Some("https://example.com".to_string()),
|
||||
};
|
||||
|
||||
let json = serde_json::to_value(&implementation).unwrap();
|
||||
assert_eq!(json["name"], "test-server");
|
||||
assert_eq!(json["websiteUrl"], "https://example.com");
|
||||
assert!(json["icons"].is_array());
|
||||
assert_eq!(json["icons"][0]["src"], "https://example.com/icon.png");
|
||||
assert_eq!(json["icons"][1]["mimeType"], "image/svg+xml");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_backward_compatibility() {
|
||||
// Test that old JSON without icons still deserializes correctly
|
||||
let old_json = json!({
|
||||
"name": "legacy-server",
|
||||
"version": "0.9.0"
|
||||
});
|
||||
|
||||
let implementation: Implementation = serde_json::from_value(old_json).unwrap();
|
||||
assert_eq!(implementation.name, "legacy-server");
|
||||
assert_eq!(implementation.version, "0.9.0");
|
||||
assert_eq!(implementation.icons, None);
|
||||
assert_eq!(implementation.website_url, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_initialize_with_icons() {
|
||||
let init_result = InitializeResult {
|
||||
protocol_version: ProtocolVersion::default(),
|
||||
capabilities: ServerCapabilities::default(),
|
||||
server_info: Implementation {
|
||||
name: "icon-server".to_string(),
|
||||
title: None,
|
||||
version: "2.0.0".to_string(),
|
||||
icons: Some(vec![Icon {
|
||||
src: "https://example.com/server.png".to_string(),
|
||||
mime_type: Some("image/png".to_string()),
|
||||
sizes: None,
|
||||
}]),
|
||||
website_url: Some("https://docs.example.com".to_string()),
|
||||
},
|
||||
instructions: None,
|
||||
};
|
||||
|
||||
let json = serde_json::to_value(&init_result).unwrap();
|
||||
assert!(json["serverInfo"]["icons"].is_array());
|
||||
assert_eq!(
|
||||
json["serverInfo"]["icons"][0]["src"],
|
||||
"https://example.com/server.png"
|
||||
);
|
||||
assert_eq!(json["serverInfo"]["websiteUrl"], "https://docs.example.com");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -257,6 +257,7 @@ mod tests {
|
|||
description: Some("A test file".to_string()),
|
||||
mime_type: Some("text/plain".to_string()),
|
||||
size: Some(100),
|
||||
icons: None,
|
||||
});
|
||||
|
||||
let json = serde_json::to_string(&resource_link).unwrap();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use base64::engine::{Engine, general_purpose::STANDARD as BASE64_STANDARD};
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{
|
||||
AnnotateAble, Annotations, RawEmbeddedResource, RawImageContent,
|
||||
AnnotateAble, Annotations, Icon, RawEmbeddedResource, RawImageContent,
|
||||
content::{EmbeddedResource, ImageContent},
|
||||
resource::ResourceContents,
|
||||
};
|
||||
|
|
@ -22,6 +22,9 @@ pub struct Prompt {
|
|||
/// Optional arguments that can be passed to customize the prompt
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub arguments: Option<Vec<PromptArgument>>,
|
||||
/// Optional list of icons for the prompt
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub icons: Option<Vec<Icon>>,
|
||||
}
|
||||
|
||||
impl Prompt {
|
||||
|
|
@ -40,6 +43,7 @@ impl Prompt {
|
|||
title: None,
|
||||
description: description.map(Into::into),
|
||||
arguments,
|
||||
icons: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{Annotated, Meta};
|
||||
use super::{Annotated, Icon, Meta};
|
||||
|
||||
/// Represents a resource in the extension with metadata
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
|
|
@ -26,6 +26,9 @@ pub struct RawResource {
|
|||
/// This can be used by Hosts to display file sizes and estimate context window us
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub size: Option<u32>,
|
||||
/// Optional list of icons for the resource
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub icons: Option<Vec<Icon>>,
|
||||
}
|
||||
|
||||
pub type Resource = Annotated<RawResource>;
|
||||
|
|
@ -91,6 +94,7 @@ impl RawResource {
|
|||
description: None,
|
||||
mime_type: None,
|
||||
size: None,
|
||||
icons: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -110,6 +114,7 @@ mod tests {
|
|||
description: Some("Test resource".to_string()),
|
||||
mime_type: Some("text/plain".to_string()),
|
||||
size: Some(100),
|
||||
icons: None,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&resource).unwrap();
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use schemars::JsonSchema;
|
|||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use super::JsonObject;
|
||||
use super::{Icon, JsonObject};
|
||||
|
||||
/// A tool that can be used by a model.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
|
|
@ -29,6 +29,9 @@ pub struct Tool {
|
|||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
/// Optional additional tool information.
|
||||
pub annotations: Option<ToolAnnotations>,
|
||||
/// Optional list of icons for the tool
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub icons: Option<Vec<Icon>>,
|
||||
}
|
||||
|
||||
/// Additional properties describing a Tool to clients.
|
||||
|
|
@ -146,6 +149,7 @@ impl Tool {
|
|||
input_schema: input_schema.into(),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
icons: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue