1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize, Default)]
11pub struct ToolDescription {
12 pub name: String,
14 pub description: String,
16 #[serde(rename = "inputSchema")]
20 pub input_schema: serde_json::Value,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize, Default)]
25pub struct ResourceDescription {
26 pub uri: String,
28 pub name: String,
30 #[serde(skip_serializing_if = "Option::is_none")]
32 pub description: Option<String>,
33 #[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
37 pub mime_type: Option<String>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize, Default)]
42pub struct PromptArgument {
43 pub name: String,
45 #[serde(skip_serializing_if = "Option::is_none")]
47 pub description: Option<String>,
48 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
50 pub required: bool,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize, Default)]
56pub struct PromptDescription {
57 pub name: String,
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub description: Option<String>,
62 #[serde(default, skip_serializing_if = "Vec::is_empty")]
64 pub arguments: Vec<PromptArgument>,
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70
71 #[test]
72 fn tool_description_serializes_camelcase_input_schema() {
73 let tool = ToolDescription {
74 name: "search".into(),
75 description: "Search documents".into(),
76 input_schema: serde_json::json!({"type": "object"}),
77 };
78 let json = serde_json::to_value(&tool).unwrap();
79 assert!(json.get("inputSchema").is_some(), "expected camelCase key");
80 assert!(json.get("input_schema").is_none());
81 }
82
83 #[test]
84 fn resource_description_serializes_camelcase_mime_type() {
85 let res = ResourceDescription {
86 uri: "docs://readme".into(),
87 name: "README".into(),
88 description: Some("Project readme".into()),
89 mime_type: Some("text/markdown".into()),
90 };
91 let json = serde_json::to_value(&res).unwrap();
92 assert_eq!(json["mimeType"], "text/markdown");
93 assert!(json.get("mime_type").is_none());
94 }
95
96 #[test]
97 fn resource_description_roundtrip() {
98 let res = ResourceDescription {
99 uri: "docs://readme".into(),
100 name: "README".into(),
101 description: Some("Project readme".into()),
102 mime_type: Some("text/markdown".into()),
103 };
104 let json = serde_json::to_string(&res).unwrap();
105 let back: ResourceDescription = serde_json::from_str(&json).unwrap();
106 assert_eq!(back.uri, "docs://readme");
107 assert_eq!(back.mime_type.as_deref(), Some("text/markdown"));
108 }
109
110 #[test]
111 fn prompt_description_serializes() {
112 let prompt = PromptDescription {
113 name: "summarize".into(),
114 description: Some("Summarize a document".into()),
115 arguments: vec![PromptArgument {
116 name: "text".into(),
117 description: Some("The text to summarize".into()),
118 required: true,
119 }],
120 };
121 let json = serde_json::to_value(&prompt).unwrap();
122 assert_eq!(json["name"], "summarize");
123 assert_eq!(json["arguments"][0]["name"], "text");
124 assert_eq!(json["arguments"][0]["required"], true);
125 }
126
127 #[test]
128 fn prompt_argument_omits_false_required() {
129 let arg = PromptArgument {
130 name: "opt".into(),
131 description: None,
132 required: false,
133 };
134 let json = serde_json::to_value(&arg).unwrap();
135 assert!(json.get("required").is_none(), "false required is omitted");
136 assert!(json.get("description").is_none());
137 }
138}