codeprism_mcp_server/
response.rs1use rmcp::model::{CallToolResult, Content};
8use serde_json::Value;
9use tracing::warn;
10
11pub fn create_dual_response(data: &Value) -> CallToolResult {
38 let text_content = Content::text(
40 serde_json::to_string_pretty(data)
41 .unwrap_or_else(|_| "Error formatting response".to_string()),
42 );
43
44 let mut content_list = vec![text_content];
47
48 match add_structured_content(data) {
50 Ok(structured_content) => {
51 content_list.push(structured_content);
52 }
53 Err(e) => {
54 warn!("Failed to add structured content: {}", e);
55 }
57 }
58
59 CallToolResult::success(content_list)
60}
61
62pub fn create_error_response(error_message: &str, error_code: Option<&str>) -> CallToolResult {
71 let error_data = serde_json::json!({
72 "status": "error",
73 "message": error_message,
74 "code": error_code
75 });
76
77 let error_text = Content::text(
79 serde_json::to_string_pretty(&error_data)
80 .unwrap_or_else(|_| format!(r#"{{"status":"error","message":"{error_message}"}}"#)),
81 );
82
83 let mut content_list = vec![error_text];
84
85 if let Ok(structured_content) = add_structured_content(&error_data) {
87 content_list.push(structured_content);
88 }
89
90 CallToolResult::error(content_list)
91}
92
93fn add_structured_content(data: &Value) -> Result<Content, Box<dyn std::error::Error>> {
104 match Content::json(data) {
107 Ok(json_content) => Ok(json_content),
108 Err(_e) => {
109 Ok(Content::text(format!(
111 "STRUCTURED_JSON:{}",
112 serde_json::to_string_pretty(data).unwrap_or_else(|_| "{}".to_string())
113 )))
114 }
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use serde_json::json;
122
123 #[test]
124 fn test_dual_response_format() {
125 let data = json!({
126 "status": "success",
127 "result": "test_value"
128 });
129
130 let response = create_dual_response(&data);
131
132 assert!(!response.content.is_empty(), "Should not be empty");
136
137 let first_content = &response.content[0];
139 if let Some(text_content) = first_content.as_text() {
140 assert!(text_content.text.contains("test_value"));
141 assert!(text_content.text.contains("success"));
142 }
143 }
144
145 #[test]
146 fn test_error_response_format() {
147 let response = create_error_response("Test error", Some("TEST_ERROR"));
148
149 assert!(!response.content.is_empty(), "Should not be empty");
151
152 let first_content = &response.content[0];
153 if let Some(text_content) = first_content.as_text() {
154 assert!(text_content.text.contains("Test error"));
155 assert!(text_content.text.contains("TEST_ERROR"));
156 }
157 }
158
159 #[test]
160 fn test_backward_compatibility() {
161 let data = json!({"test": "value"});
162 let response = create_dual_response(&data);
163
164 let text_content = &response.content[0];
166 if let Some(text) = text_content.as_text() {
167 assert!(text.text.contains("\"test\""));
168 assert!(text.text.contains("\"value\""));
169 }
170 }
171
172 #[test]
173 fn test_complex_json_data() {
174 let data = json!({
175 "status": "success",
176 "repository_overview": {
177 "total_files": 42,
178 "languages": ["rust", "python"],
179 "complexity": {
180 "average": 3.2,
181 "max": 15
182 }
183 }
184 });
185
186 let response = create_dual_response(&data);
187
188 assert!(!response.content.is_empty(), "Should not be empty");
190
191 let first_content = &response.content[0];
192 if let Some(text_content) = first_content.as_text() {
193 assert!(text_content.text.contains("repository_overview"));
194 assert!(text_content.text.contains("total_files"));
195 assert!(text_content.text.contains("42"));
196 }
197 }
198
199 #[test]
200 fn test_dual_response_contains_multiple_content_items() {
201 let data = json!({
202 "status": "success",
203 "message": "ping response"
204 });
205
206 let response = create_dual_response(&data);
207
208 assert!(!response.content.is_empty(), "Should not be empty");
211
212 let first_content = &response.content[0];
214 assert!(
215 first_content.as_text().is_some(),
216 "First content should be text for backward compatibility"
217 );
218 let text_content = first_content.as_text().unwrap();
219 assert!(
220 !text_content.text.is_empty(),
221 "Text content should not be empty"
222 );
223 assert!(
224 text_content.text.contains("ping response"),
225 "Text should contain the actual test data"
226 );
227 }
228
229 #[test]
230 fn test_structured_response_for_comprehensive_specs() {
231 let data = json!({
233 "status": "success",
234 "quality_metrics": {
235 "overall_score": 8.5,
236 "maintainability": 9.0,
237 "complexity": 7.0
238 },
239 "code_smells": [],
240 "recommendations": ["Excellent code quality"]
241 });
242
243 let response = create_dual_response(&data);
244
245 assert!(!response.content.is_empty(), "Should not be empty");
247
248 let first_content = &response.content[0];
250 if let Some(text_content) = first_content.as_text() {
251 assert!(text_content.text.contains("quality_metrics"));
252 assert!(text_content.text.contains("overall_score"));
253 assert!(text_content.text.contains("8.5"));
254 }
255
256 if response.content.len() > 1 {
258 let second_content = &response.content[1];
261 let has_text = second_content.as_text().is_some();
263 let has_resource = second_content.as_resource().is_some();
264 assert!(
265 has_text || has_resource,
266 "Second content should be either text or resource"
267 );
268
269 if has_text {
271 let text = second_content.as_text().unwrap();
272 assert!(
273 !text.text.is_empty(),
274 "Structured text content should not be empty"
275 );
276 }
277 if has_resource {
278 let resource = second_content.as_resource().unwrap();
279 let uri = match &resource.resource {
281 rmcp::model::ResourceContents::TextResourceContents { uri, .. } => uri,
282 rmcp::model::ResourceContents::BlobResourceContents { uri, .. } => uri,
283 };
284 assert!(!uri.is_empty(), "Resource URI should not be empty");
285 }
286 }
287 }
288}