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 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
54 pub arg_type: Option<String>,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize, Default)]
60pub struct PromptDescription {
61 pub name: String,
63 #[serde(skip_serializing_if = "Option::is_none")]
65 pub description: Option<String>,
66 #[serde(default, skip_serializing_if = "Vec::is_empty")]
68 pub arguments: Vec<PromptArgument>,
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74
75 #[test]
76 fn tool_description_serializes_camelcase_input_schema() {
77 let tool = ToolDescription {
78 name: "search".into(),
79 description: "Search documents".into(),
80 input_schema: serde_json::json!({"type": "object"}),
81 };
82 let json = serde_json::to_value(&tool).unwrap();
83 assert!(json.get("inputSchema").is_some(), "expected camelCase key");
84 assert!(json.get("input_schema").is_none());
85 }
86
87 #[test]
88 fn resource_description_serializes_camelcase_mime_type() {
89 let res = ResourceDescription {
90 uri: "docs://readme".into(),
91 name: "README".into(),
92 description: Some("Project readme".into()),
93 mime_type: Some("text/markdown".into()),
94 };
95 let json = serde_json::to_value(&res).unwrap();
96 assert_eq!(json["mimeType"], "text/markdown");
97 assert!(json.get("mime_type").is_none());
98 }
99
100 #[test]
101 fn resource_description_roundtrip() {
102 let res = ResourceDescription {
103 uri: "docs://readme".into(),
104 name: "README".into(),
105 description: Some("Project readme".into()),
106 mime_type: Some("text/markdown".into()),
107 };
108 let json = serde_json::to_string(&res).unwrap();
109 let back: ResourceDescription = serde_json::from_str(&json).unwrap();
110 assert_eq!(back.uri, "docs://readme");
111 assert_eq!(back.mime_type.as_deref(), Some("text/markdown"));
112 }
113
114 #[test]
115 fn prompt_description_serializes() {
116 let prompt = PromptDescription {
117 name: "summarize".into(),
118 description: Some("Summarize a document".into()),
119 arguments: vec![PromptArgument {
120 name: "text".into(),
121 description: Some("The text to summarize".into()),
122 required: true,
123 arg_type: Some("string".into()),
124 }],
125 };
126 let json = serde_json::to_value(&prompt).unwrap();
127 assert_eq!(json["name"], "summarize");
128 assert_eq!(json["arguments"][0]["name"], "text");
129 assert_eq!(json["arguments"][0]["required"], true);
130 assert_eq!(json["arguments"][0]["type"], "string");
131 }
132
133 #[test]
134 fn prompt_argument_omits_false_required() {
135 let arg = PromptArgument {
136 name: "opt".into(),
137 description: None,
138 required: false,
139 arg_type: None,
140 };
141 let json = serde_json::to_value(&arg).unwrap();
142 assert!(json.get("required").is_none(), "false required is omitted");
143 assert!(json.get("description").is_none());
144 assert!(json.get("type").is_none(), "untyped argument omits type");
145 }
146}