use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct ServerInfo {
pub name: String,
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icons: Option<Vec<Icon>>,
#[serde(rename = "websiteUrl", skip_serializing_if = "Option::is_none")]
pub website_url: Option<String>,
}
impl ServerInfo {
#[must_use]
pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
Self {
name: name.into(),
version: version.into(),
..Default::default()
}
}
#[must_use]
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
#[must_use]
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
#[must_use]
pub fn with_icon(mut self, icon: Icon) -> Self {
self.icons.get_or_insert_with(Vec::new).push(icon);
self
}
#[must_use]
pub fn with_website_url(mut self, url: impl Into<String>) -> Self {
self.website_url = Some(url.into());
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct Icon {
pub src: String,
#[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sizes: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub theme: Option<IconTheme>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum IconTheme {
Light,
Dark,
}
impl std::fmt::Display for IconTheme {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Light => f.write_str("light"),
Self::Dark => f.write_str("dark"),
}
}
}
impl Icon {
#[must_use]
pub fn new(src: impl Into<String>) -> Self {
Self {
src: src.into(),
..Default::default()
}
}
#[must_use]
pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
self.mime_type = Some(mime_type.into());
self
}
#[must_use]
pub fn with_sizes(mut self, sizes: Vec<impl Into<String>>) -> Self {
self.sizes = Some(sizes.into_iter().map(Into::into).collect());
self
}
#[must_use]
pub fn with_theme(mut self, theme: IconTheme) -> Self {
self.theme = Some(theme);
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct Tool {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(rename = "inputSchema")]
pub input_schema: ToolInputSchema,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icons: Option<Vec<Icon>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<ToolAnnotations>,
#[serde(skip_serializing_if = "Option::is_none")]
pub execution: Option<ToolExecution>,
#[serde(rename = "outputSchema", skip_serializing_if = "Option::is_none")]
pub output_schema: Option<Value>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<std::collections::HashMap<String, Value>>,
}
impl Tool {
#[must_use]
pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
Self {
name: name.into(),
description: Some(description.into()),
input_schema: ToolInputSchema::default(),
..Default::default()
}
}
#[must_use]
pub fn with_schema(mut self, schema: ToolInputSchema) -> Self {
self.input_schema = schema;
self
}
#[must_use]
pub fn with_output_schema(mut self, schema: Value) -> Self {
self.output_schema = Some(schema);
self
}
#[must_use]
pub fn with_annotations(mut self, annotations: ToolAnnotations) -> Self {
self.annotations = Some(annotations);
self
}
#[must_use]
pub fn with_icon(mut self, icon: Icon) -> Self {
self.icons.get_or_insert_with(Vec::new).push(icon);
self
}
#[must_use]
pub fn with_execution(mut self, execution: ToolExecution) -> Self {
self.execution = Some(execution);
self
}
#[must_use]
pub fn read_only(mut self) -> Self {
self.annotations = Some(self.annotations.unwrap_or_default().with_read_only(true));
self
}
#[must_use]
pub fn destructive(mut self) -> Self {
self.annotations = Some(self.annotations.unwrap_or_default().with_destructive(true));
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct ToolExecution {
#[serde(rename = "taskSupport", skip_serializing_if = "Option::is_none")]
pub task_support: Option<TaskSupportLevel>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum TaskSupportLevel {
Forbidden,
Optional,
Required,
}
impl std::fmt::Display for TaskSupportLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Forbidden => f.write_str("forbidden"),
Self::Optional => f.write_str("optional"),
Self::Required => f.write_str("required"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ToolInputSchema {
#[serde(rename = "type")]
pub schema_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub properties: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<Vec<String>>,
#[serde(
rename = "additionalProperties",
skip_serializing_if = "Option::is_none"
)]
pub additional_properties: Option<bool>,
}
impl Default for ToolInputSchema {
fn default() -> Self {
Self {
schema_type: "object".into(),
properties: None,
required: None,
additional_properties: Some(false),
}
}
}
impl ToolInputSchema {
#[must_use]
pub fn empty() -> Self {
Self::default()
}
#[must_use]
pub fn from_value(value: Value) -> Self {
serde_json::from_value(value).unwrap_or_default()
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct ToolAnnotations {
#[serde(rename = "readOnlyHint", skip_serializing_if = "Option::is_none")]
pub read_only_hint: Option<bool>,
#[serde(rename = "destructiveHint", skip_serializing_if = "Option::is_none")]
pub destructive_hint: Option<bool>,
#[serde(rename = "idempotentHint", skip_serializing_if = "Option::is_none")]
pub idempotent_hint: Option<bool>,
#[serde(rename = "openWorldHint", skip_serializing_if = "Option::is_none")]
pub open_world_hint: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
}
impl ToolAnnotations {
#[must_use]
pub fn with_read_only(mut self, value: bool) -> Self {
self.read_only_hint = Some(value);
self
}
#[must_use]
pub fn with_destructive(mut self, value: bool) -> Self {
self.destructive_hint = Some(value);
self
}
#[must_use]
pub fn with_idempotent(mut self, value: bool) -> Self {
self.idempotent_hint = Some(value);
self
}
#[must_use]
pub fn with_open_world(mut self, value: bool) -> Self {
self.open_world_hint = Some(value);
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct Resource {
pub uri: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icons: Option<Vec<Icon>>,
#[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<ResourceAnnotations>,
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<u64>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<std::collections::HashMap<String, Value>>,
}
impl Resource {
#[must_use]
pub fn new(uri: impl Into<String>, name: impl Into<String>) -> Self {
Self {
uri: uri.into(),
name: name.into(),
..Default::default()
}
}
#[must_use]
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
#[must_use]
pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
self.mime_type = Some(mime_type.into());
self
}
#[must_use]
pub fn with_size(mut self, size: u64) -> Self {
self.size = Some(size);
self
}
#[must_use]
pub fn with_icon(mut self, icon: Icon) -> Self {
self.icons.get_or_insert_with(Vec::new).push(icon);
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct ResourceAnnotations {
#[serde(skip_serializing_if = "Option::is_none")]
pub audience: Option<Vec<crate::Role>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<f64>,
#[serde(rename = "lastModified", skip_serializing_if = "Option::is_none")]
pub last_modified: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct ResourceTemplate {
#[serde(rename = "uriTemplate")]
pub uri_template: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icons: Option<Vec<Icon>>,
#[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<ResourceAnnotations>,
}
impl ResourceTemplate {
#[must_use]
pub fn new(uri_template: impl Into<String>, name: impl Into<String>) -> Self {
Self {
uri_template: uri_template.into(),
name: name.into(),
..Default::default()
}
}
#[must_use]
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
#[must_use]
pub fn with_icon(mut self, icon: Icon) -> Self {
self.icons.get_or_insert_with(Vec::new).push(icon);
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct Prompt {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icons: Option<Vec<Icon>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<Vec<PromptArgument>>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<std::collections::HashMap<String, Value>>,
}
impl Prompt {
#[must_use]
pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
Self {
name: name.into(),
description: Some(description.into()),
..Default::default()
}
}
#[must_use]
pub fn with_argument(mut self, arg: PromptArgument) -> Self {
self.arguments.get_or_insert_with(Vec::new).push(arg);
self
}
#[must_use]
pub fn with_required_arg(
self,
name: impl Into<String>,
description: impl Into<String>,
) -> Self {
self.with_argument(PromptArgument::required(name, description))
}
#[must_use]
pub fn with_optional_arg(
self,
name: impl Into<String>,
description: impl Into<String>,
) -> Self {
self.with_argument(PromptArgument::optional(name, description))
}
#[must_use]
pub fn with_icon(mut self, icon: Icon) -> Self {
self.icons.get_or_insert_with(Vec::new).push(icon);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PromptArgument {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<bool>,
}
impl PromptArgument {
#[must_use]
pub fn required(name: impl Into<String>, description: impl Into<String>) -> Self {
Self {
name: name.into(),
title: None,
description: Some(description.into()),
required: Some(true),
}
}
#[must_use]
pub fn optional(name: impl Into<String>, description: impl Into<String>) -> Self {
Self {
name: name.into(),
title: None,
description: Some(description.into()),
required: Some(false),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_server_info() {
let info = ServerInfo::new("my-server", "1.0.0")
.with_title("My Server")
.with_description("A test server")
.with_icon(Icon::new("https://example.com/icon.png"));
assert_eq!(info.name, "my-server");
assert_eq!(info.version, "1.0.0");
assert_eq!(info.title, Some("My Server".into()));
assert_eq!(info.icons.as_ref().unwrap().len(), 1);
assert_eq!(
info.icons.as_ref().unwrap()[0].src,
"https://example.com/icon.png"
);
}
#[test]
fn test_tool_builder() {
let tool = Tool::new("add", "Add two numbers").with_annotations(
ToolAnnotations::default()
.with_read_only(true)
.with_idempotent(true),
);
assert_eq!(tool.name, "add");
assert!(tool.annotations.as_ref().unwrap().read_only_hint.unwrap());
assert!(tool.annotations.as_ref().unwrap().idempotent_hint.unwrap());
}
#[test]
fn test_tool_read_only() {
let tool = Tool::new("query", "Query data").read_only();
assert!(tool.annotations.as_ref().unwrap().read_only_hint.unwrap());
}
#[test]
fn test_tool_destructive() {
let tool = Tool::new("delete", "Delete data").destructive();
assert!(tool.annotations.as_ref().unwrap().destructive_hint.unwrap());
}
#[test]
fn test_resource_builder() {
let resource = Resource::new("file:///test.txt", "test")
.with_description("A test file")
.with_mime_type("text/plain");
assert_eq!(resource.uri, "file:///test.txt");
assert_eq!(resource.mime_type, Some("text/plain".into()));
}
#[test]
fn test_prompt_builder() {
let prompt = Prompt::new("greeting", "A greeting prompt")
.with_required_arg("name", "Name to greet")
.with_optional_arg("style", "Greeting style");
assert_eq!(prompt.name, "greeting");
assert_eq!(prompt.arguments.as_ref().unwrap().len(), 2);
assert!(prompt.arguments.as_ref().unwrap()[0].required.unwrap());
assert!(!prompt.arguments.as_ref().unwrap()[1].required.unwrap());
}
#[test]
fn test_tool_serde() {
let tool = Tool::new("test", "Test tool");
let json = serde_json::to_string(&tool).unwrap();
assert!(json.contains("\"name\":\"test\""));
assert!(json.contains("\"inputSchema\""));
}
}