1use super::types::*;
6use crate::hf::hub_client::{HubAssetType, HubClient, SearchFilters};
7use std::collections::HashMap;
8
9pub struct McpServer {
11 hub_client: HubClient,
12}
13
14impl McpServer {
15 pub fn new() -> Self {
17 Self { hub_client: HubClient::new() }
18 }
19
20 pub fn handle_request(&mut self, request: &JsonRpcRequest) -> JsonRpcResponse {
22 match request.method.as_str() {
23 "initialize" => self.handle_initialize(request),
24 "tools/list" => self.handle_tools_list(request),
25 "tools/call" => self.handle_tools_call(request),
26 _ => JsonRpcResponse::error(
27 request.id.clone(),
28 -32601,
29 format!("Method not found: {}", request.method),
30 ),
31 }
32 }
33
34 fn handle_initialize(&self, request: &JsonRpcRequest) -> JsonRpcResponse {
36 JsonRpcResponse::success(
37 request.id.clone(),
38 serde_json::json!({
39 "protocolVersion": "2024-11-05",
40 "capabilities": {
41 "tools": { "listChanged": false }
42 },
43 "serverInfo": {
44 "name": "batuta-hf",
45 "version": env!("CARGO_PKG_VERSION")
46 }
47 }),
48 )
49 }
50
51 fn handle_tools_list(&self, request: &JsonRpcRequest) -> JsonRpcResponse {
53 let tools = self.tool_definitions();
54 JsonRpcResponse::success(request.id.clone(), serde_json::json!({ "tools": tools }))
55 }
56
57 fn handle_tools_call(&mut self, request: &JsonRpcRequest) -> JsonRpcResponse {
59 let name = request.params.get("name").and_then(|v| v.as_str());
60 let arguments = request.params.get("arguments").cloned().unwrap_or(serde_json::json!({}));
61
62 let result = match name {
63 Some("hf_search") => self.tool_hf_search(&arguments),
64 Some("hf_info") => self.tool_hf_info(&arguments),
65 Some("hf_tree") => self.tool_hf_tree(&arguments),
66 Some("hf_integration") => self.tool_hf_integration(),
67 Some(other) => ToolCallResult::error(format!("Unknown tool: {}", other)),
68 None => ToolCallResult::error("Missing tool name"),
69 };
70
71 JsonRpcResponse::success(
72 request.id.clone(),
73 serde_json::to_value(result).unwrap_or(serde_json::json!({})),
74 )
75 }
76
77 fn tool_definitions(&self) -> Vec<ToolDefinition> {
83 vec![
84 ToolDefinition {
85 name: "hf_search".to_string(),
86 description: "Search HuggingFace Hub for models, datasets, or spaces".to_string(),
87 input_schema: InputSchema {
88 schema_type: "object".to_string(),
89 properties: HashMap::from([
90 (
91 "query".to_string(),
92 PropertySchema {
93 prop_type: "string".to_string(),
94 description: "Search query text".to_string(),
95 r#enum: None,
96 },
97 ),
98 (
99 "asset_type".to_string(),
100 PropertySchema {
101 prop_type: "string".to_string(),
102 description: "Type of asset to search".to_string(),
103 r#enum: Some(vec![
104 "model".into(),
105 "dataset".into(),
106 "space".into(),
107 ]),
108 },
109 ),
110 (
111 "task".to_string(),
112 PropertySchema {
113 prop_type: "string".to_string(),
114 description: "Filter by ML task (e.g., text-generation)"
115 .to_string(),
116 r#enum: None,
117 },
118 ),
119 (
120 "limit".to_string(),
121 PropertySchema {
122 prop_type: "integer".to_string(),
123 description: "Maximum number of results (default: 10)".to_string(),
124 r#enum: None,
125 },
126 ),
127 ]),
128 required: vec!["query".to_string()],
129 },
130 },
131 ToolDefinition {
132 name: "hf_info".to_string(),
133 description: "Get metadata for a HuggingFace model, dataset, or space".to_string(),
134 input_schema: InputSchema {
135 schema_type: "object".to_string(),
136 properties: HashMap::from([
137 (
138 "repo_id".to_string(),
139 PropertySchema {
140 prop_type: "string".to_string(),
141 description: "Repository ID (e.g., meta-llama/Llama-2-7b-hf)"
142 .to_string(),
143 r#enum: None,
144 },
145 ),
146 (
147 "asset_type".to_string(),
148 PropertySchema {
149 prop_type: "string".to_string(),
150 description: "Type of asset".to_string(),
151 r#enum: Some(vec![
152 "model".into(),
153 "dataset".into(),
154 "space".into(),
155 ]),
156 },
157 ),
158 ]),
159 required: vec!["repo_id".to_string()],
160 },
161 },
162 ToolDefinition {
163 name: "hf_tree".to_string(),
164 description: "Show HuggingFace ecosystem component hierarchy".to_string(),
165 input_schema: InputSchema {
166 schema_type: "object".to_string(),
167 properties: HashMap::from([(
168 "category".to_string(),
169 PropertySchema {
170 prop_type: "string".to_string(),
171 description: "Filter by category (e.g., inference, training)"
172 .to_string(),
173 r#enum: None,
174 },
175 )]),
176 required: vec![],
177 },
178 },
179 ToolDefinition {
180 name: "hf_integration".to_string(),
181 description: "Show PAIML stack to HuggingFace integration mappings".to_string(),
182 input_schema: InputSchema {
183 schema_type: "object".to_string(),
184 properties: HashMap::new(),
185 required: vec![],
186 },
187 },
188 ]
189 }
190
191 fn tool_hf_search(&mut self, args: &serde_json::Value) -> ToolCallResult {
196 let query = args.get("query").and_then(|v| v.as_str()).unwrap_or("");
197 let asset_type = args.get("asset_type").and_then(|v| v.as_str()).unwrap_or("model");
198 let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
199 let task = args.get("task").and_then(|v| v.as_str());
200
201 let mut filters = SearchFilters::new().with_query(query).with_limit(limit);
202 if let Some(t) = task {
203 filters = filters.with_task(t);
204 }
205
206 let results = match asset_type {
207 "model" => self.hub_client.search_models(&filters),
208 "dataset" => self.hub_client.search_datasets(&filters),
209 "space" => self.hub_client.search_spaces(&filters),
210 _ => return ToolCallResult::error(format!("Invalid asset_type: {}", asset_type)),
211 };
212
213 match results {
214 Ok(assets) => {
215 let formatted: Vec<String> = assets
216 .iter()
217 .map(|a| {
218 let mut line = format!("{} ({}⬇ {}♥)", a.id, a.downloads, a.likes);
219 if let Some(ref tag) = a.pipeline_tag {
220 line.push_str(&format!(" [{}]", tag));
221 }
222 line
223 })
224 .collect();
225 ToolCallResult::success(formatted.join("\n"))
226 }
227 Err(e) => ToolCallResult::error(format!("Search failed: {}", e)),
228 }
229 }
230
231 fn tool_hf_info(&mut self, args: &serde_json::Value) -> ToolCallResult {
232 let repo_id = match args.get("repo_id").and_then(|v| v.as_str()) {
233 Some(id) => id,
234 None => return ToolCallResult::error("Missing required parameter: repo_id"),
235 };
236 let asset_type = args.get("asset_type").and_then(|v| v.as_str()).unwrap_or("model");
237
238 let result = match asset_type {
239 "model" => self.hub_client.get_model(repo_id),
240 "dataset" => self.hub_client.get_dataset(repo_id),
241 "space" => self.hub_client.get_space(repo_id),
242 _ => return ToolCallResult::error(format!("Invalid asset_type: {}", asset_type)),
243 };
244
245 match result {
246 Ok(asset) => {
247 let mut info = format!("ID: {}\n", asset.id);
248 info.push_str(&format!("Author: {}\n", asset.author));
249 info.push_str(&format!("Downloads: {}\n", asset.downloads));
250 info.push_str(&format!("Likes: {}\n", asset.likes));
251 if let Some(ref tag) = asset.pipeline_tag {
252 info.push_str(&format!("Task: {}\n", tag));
253 }
254 if let Some(ref lib) = asset.library {
255 info.push_str(&format!("Library: {}\n", lib));
256 }
257 if let Some(ref license) = asset.license {
258 info.push_str(&format!("License: {}\n", license));
259 }
260 if !asset.tags.is_empty() {
261 info.push_str(&format!("Tags: {}\n", asset.tags.join(", ")));
262 }
263 ToolCallResult::success(info)
264 }
265 Err(e) => ToolCallResult::error(format!("Info failed: {}", e)),
266 }
267 }
268
269 fn tool_hf_tree(&self, args: &serde_json::Value) -> ToolCallResult {
270 let _category = args.get("category").and_then(|v| v.as_str());
271
272 let tree = r"HuggingFace Ecosystem
273├── Inference
274│ ├── transformers (PyTorch/TF models)
275│ ├── text-generation-inference (TGI)
276│ ├── optimum (hardware optimization)
277│ └── candle (Rust inference)
278├── Training
279│ ├── accelerate (distributed training)
280│ ├── peft (parameter-efficient fine-tuning)
281│ ├── trl (RLHF training)
282│ └── bitsandbytes (quantization)
283├── Data
284│ ├── datasets (data loading)
285│ ├── tokenizers (fast tokenization)
286│ └── evaluate (metrics)
287├── Deployment
288│ ├── inference-endpoints (managed API)
289│ ├── spaces (app hosting)
290│ └── gradio (web UI)
291└── PAIML Integration
292 ├── trueno ↔ candle (tensor ops)
293 ├── aprender ↔ transformers (ML algorithms)
294 ├── realizar ↔ TGI (inference serving)
295 └── alimentar ↔ datasets (data loading)";
296
297 ToolCallResult::success(tree)
298 }
299
300 fn tool_hf_integration(&self) -> ToolCallResult {
301 let map = r"PAIML ↔ HuggingFace Integration Map
302
303| PAIML Component | HF Equivalent | Integration |
304|-----------------|---------------|-------------|
305| trueno | candle | SIMD tensor operations |
306| aprender | transformers | ML algorithm mapping |
307| realizar | TGI | Inference serving |
308| alimentar | datasets | Data loading (Arrow) |
309| entrenar | accelerate | Distributed training |
310| entrenar | peft | LoRA/QLoRA fine-tuning |
311| entrenar | trl | RLHF training |
312| whisper-apr | whisper | Speech recognition |
313| pacha | hub | Model registry |
314| batuta | gradio | UI/deployment |
315
316Format: SafeTensors (shared), APR v2 (PAIML native)
317Quantization: Q4K/Q5K/Q6K (PAIML) ↔ GPTQ/AWQ (HF)";
318
319 ToolCallResult::success(map)
320 }
321
322 #[cfg(feature = "native")]
324 pub fn run_stdio(&mut self) -> anyhow::Result<()> {
325 use std::io::{self, BufRead, Write};
326
327 let stdin = io::stdin();
328 let stdout = io::stdout();
329
330 for line in stdin.lock().lines() {
331 let line = line?;
332 if line.trim().is_empty() {
333 continue;
334 }
335
336 let request: JsonRpcRequest = match serde_json::from_str(&line) {
337 Ok(req) => req,
338 Err(e) => {
339 let error_resp =
340 JsonRpcResponse::error(None, -32700, format!("Parse error: {}", e));
341 let json = serde_json::to_string(&error_resp)?;
342 writeln!(stdout.lock(), "{}", json)?;
343 continue;
344 }
345 };
346
347 let response = self.handle_request(&request);
348 let json = serde_json::to_string(&response)?;
349 writeln!(stdout.lock(), "{}", json)?;
350 stdout.lock().flush()?;
351 }
352
353 Ok(())
354 }
355}
356
357impl Default for McpServer {
358 fn default() -> Self {
359 Self::new()
360 }
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366
367 fn make_request(method: &str, params: serde_json::Value) -> JsonRpcRequest {
368 JsonRpcRequest {
369 jsonrpc: "2.0".to_string(),
370 id: Some(serde_json::json!(1)),
371 method: method.to_string(),
372 params,
373 }
374 }
375
376 #[test]
377 fn test_initialize() {
378 let mut server = McpServer::new();
379 let req = make_request("initialize", serde_json::json!({}));
380 let resp = server.handle_request(&req);
381 assert!(resp.result.is_some());
382 let result = resp.result.expect("operation failed");
383 assert_eq!(result["protocolVersion"], "2024-11-05");
384 assert_eq!(result["serverInfo"]["name"], "batuta-hf");
385 }
386
387 #[test]
388 fn test_tools_list() {
389 let mut server = McpServer::new();
390 let req = make_request("tools/list", serde_json::json!({}));
391 let resp = server.handle_request(&req);
392 assert!(resp.result.is_some());
393 let result = resp.result.expect("operation failed");
394 let tools = result["tools"].as_array().expect("expected array value");
395 assert_eq!(tools.len(), 4);
396 assert_eq!(tools[0]["name"], "hf_search");
397 assert_eq!(tools[1]["name"], "hf_info");
398 assert_eq!(tools[2]["name"], "hf_tree");
399 assert_eq!(tools[3]["name"], "hf_integration");
400 }
401
402 #[test]
403 fn test_tool_hf_search() {
404 let mut server = McpServer::new();
405 let req = make_request(
406 "tools/call",
407 serde_json::json!({
408 "name": "hf_search",
409 "arguments": {
410 "query": "llama",
411 "asset_type": "model",
412 "limit": 5
413 }
414 }),
415 );
416 let resp = server.handle_request(&req);
417 assert!(resp.result.is_some());
418 let result = resp.result.expect("operation failed");
419 assert!(result["isError"].is_null());
420 let content = result["content"].as_array().expect("expected array value");
421 assert!(!content.is_empty());
422 assert!(content[0]["text"].as_str().expect("expected string value").contains("llama"));
423 }
424
425 #[test]
426 fn test_tool_hf_info() {
427 let mut server = McpServer::new();
428 let req = make_request(
429 "tools/call",
430 serde_json::json!({
431 "name": "hf_info",
432 "arguments": {
433 "repo_id": "meta-llama/Llama-2-7b-hf",
434 "asset_type": "model"
435 }
436 }),
437 );
438 let resp = server.handle_request(&req);
439 assert!(resp.result.is_some());
440 let result = resp.result.expect("operation failed");
441 let content = result["content"].as_array().expect("expected array value");
442 assert!(content[0]["text"].as_str().expect("expected string value").contains("meta-llama"));
443 }
444
445 #[test]
446 fn test_tool_hf_tree() {
447 let mut server = McpServer::new();
448 let req = make_request(
449 "tools/call",
450 serde_json::json!({
451 "name": "hf_tree",
452 "arguments": {}
453 }),
454 );
455 let resp = server.handle_request(&req);
456 assert!(resp.result.is_some());
457 let result = resp.result.expect("operation failed");
458 let content = result["content"].as_array().expect("expected array value");
459 assert!(content[0]["text"]
460 .as_str()
461 .expect("unexpected failure")
462 .contains("HuggingFace Ecosystem"));
463 }
464
465 #[test]
466 fn test_tool_hf_integration() {
467 let mut server = McpServer::new();
468 let req = make_request(
469 "tools/call",
470 serde_json::json!({
471 "name": "hf_integration",
472 "arguments": {}
473 }),
474 );
475 let resp = server.handle_request(&req);
476 assert!(resp.result.is_some());
477 let result = resp.result.expect("operation failed");
478 let content = result["content"].as_array().expect("expected array value");
479 assert!(content[0]["text"].as_str().expect("expected string value").contains("PAIML"));
480 }
481
482 #[test]
483 fn test_unknown_method() {
484 let mut server = McpServer::new();
485 let req = make_request("unknown/method", serde_json::json!({}));
486 let resp = server.handle_request(&req);
487 assert!(resp.error.is_some());
488 assert_eq!(resp.error.expect("unexpected failure").code, -32601);
489 }
490
491 #[test]
492 fn test_unknown_tool() {
493 let mut server = McpServer::new();
494 let req = make_request(
495 "tools/call",
496 serde_json::json!({
497 "name": "nonexistent_tool",
498 "arguments": {}
499 }),
500 );
501 let resp = server.handle_request(&req);
502 assert!(resp.result.is_some());
503 let result = resp.result.expect("operation failed");
504 assert_eq!(result["isError"], true);
505 }
506
507 #[test]
508 fn test_missing_tool_name() {
509 let mut server = McpServer::new();
510 let req = make_request("tools/call", serde_json::json!({}));
511 let resp = server.handle_request(&req);
512 assert!(resp.result.is_some());
513 let result = resp.result.expect("operation failed");
514 assert_eq!(result["isError"], true);
515 }
516
517 #[test]
518 fn test_hf_info_missing_repo_id() {
519 let mut server = McpServer::new();
520 let req = make_request(
521 "tools/call",
522 serde_json::json!({
523 "name": "hf_info",
524 "arguments": {}
525 }),
526 );
527 let resp = server.handle_request(&req);
528 let result = resp.result.expect("operation failed");
529 assert_eq!(result["isError"], true);
530 assert!(result["content"][0]["text"]
531 .as_str()
532 .expect("unexpected failure")
533 .contains("Missing required"));
534 }
535
536 #[test]
537 fn test_default_impl() {
538 let server = McpServer::default();
539 assert_eq!(server.tool_definitions().len(), 4);
540 }
541
542 #[test]
547 fn test_tool_definitions_hf_search_schema() {
548 let server = McpServer::new();
549 let defs = server.tool_definitions();
550 let search = &defs[0];
551 assert_eq!(search.name, "hf_search");
552 assert!(search.description.contains("Search"));
553 assert_eq!(search.input_schema.schema_type, "object");
554 assert!(search.input_schema.required.contains(&"query".to_string()));
555
556 let props = &search.input_schema.properties;
558 assert!(props.contains_key("query"));
559 assert!(props.contains_key("asset_type"));
560 assert!(props.contains_key("task"));
561 assert!(props.contains_key("limit"));
562
563 let asset_type = props.get("asset_type").expect("key not found");
565 let enums = asset_type.r#enum.as_ref().expect("unexpected failure");
566 assert!(enums.contains(&"model".to_string()));
567 assert!(enums.contains(&"dataset".to_string()));
568 assert!(enums.contains(&"space".to_string()));
569
570 assert!(props.get("query").expect("key not found").r#enum.is_none());
572 assert!(props.get("task").expect("key not found").r#enum.is_none());
573 assert!(props.get("limit").expect("key not found").r#enum.is_none());
574
575 assert_eq!(props.get("query").expect("key not found").prop_type, "string");
577 assert_eq!(props.get("limit").expect("key not found").prop_type, "integer");
578 }
579
580 #[test]
581 fn test_tool_definitions_hf_info_schema() {
582 let server = McpServer::new();
583 let defs = server.tool_definitions();
584 let info = &defs[1];
585 assert_eq!(info.name, "hf_info");
586 assert!(info.description.contains("metadata"));
587 assert!(info.input_schema.required.contains(&"repo_id".to_string()));
588
589 let props = &info.input_schema.properties;
590 assert!(props.contains_key("repo_id"));
591 assert!(props.contains_key("asset_type"));
592 assert_eq!(props.len(), 2);
593
594 let enums =
596 props.get("asset_type").expect("key not found").r#enum.as_ref().expect("key not found");
597 assert_eq!(enums.len(), 3);
598 }
599
600 #[test]
601 fn test_tool_definitions_hf_tree_schema() {
602 let server = McpServer::new();
603 let defs = server.tool_definitions();
604 let tree = &defs[2];
605 assert_eq!(tree.name, "hf_tree");
606 assert!(tree.description.contains("hierarchy"));
607 assert!(tree.input_schema.required.is_empty());
608
609 let props = &tree.input_schema.properties;
610 assert_eq!(props.len(), 1);
611 assert!(props.contains_key("category"));
612 }
613
614 #[test]
615 fn test_tool_definitions_hf_integration_schema() {
616 let server = McpServer::new();
617 let defs = server.tool_definitions();
618 let integration = &defs[3];
619 assert_eq!(integration.name, "hf_integration");
620 assert!(integration.description.contains("integration"));
621 assert!(integration.input_schema.required.is_empty());
622 assert!(integration.input_schema.properties.is_empty());
623 }
624
625 #[test]
630 fn test_tool_hf_search_with_task_filter() {
631 let mut server = McpServer::new();
632 let req = make_request(
633 "tools/call",
634 serde_json::json!({
635 "name": "hf_search",
636 "arguments": {
637 "query": "bert",
638 "asset_type": "model",
639 "task": "text-classification",
640 "limit": 3
641 }
642 }),
643 );
644 let resp = server.handle_request(&req);
645 assert!(resp.result.is_some());
646 let result = resp.result.expect("operation failed");
647 assert!(result["isError"].is_null() || result["isError"] == true);
649 }
650
651 #[test]
652 fn test_tool_hf_search_dataset() {
653 let mut server = McpServer::new();
654 let req = make_request(
655 "tools/call",
656 serde_json::json!({
657 "name": "hf_search",
658 "arguments": {
659 "query": "squad",
660 "asset_type": "dataset",
661 "limit": 2
662 }
663 }),
664 );
665 let resp = server.handle_request(&req);
666 assert!(resp.result.is_some());
667 }
668
669 #[test]
670 fn test_tool_hf_search_space() {
671 let mut server = McpServer::new();
672 let req = make_request(
673 "tools/call",
674 serde_json::json!({
675 "name": "hf_search",
676 "arguments": {
677 "query": "gradio",
678 "asset_type": "space",
679 "limit": 2
680 }
681 }),
682 );
683 let resp = server.handle_request(&req);
684 assert!(resp.result.is_some());
685 }
686
687 #[test]
688 fn test_tool_hf_search_invalid_asset_type() {
689 let mut server = McpServer::new();
690 let req = make_request(
691 "tools/call",
692 serde_json::json!({
693 "name": "hf_search",
694 "arguments": {
695 "query": "test",
696 "asset_type": "invalid_type"
697 }
698 }),
699 );
700 let resp = server.handle_request(&req);
701 let result = resp.result.expect("operation failed");
702 assert_eq!(result["isError"], true);
703 assert!(result["content"][0]["text"]
704 .as_str()
705 .expect("unexpected failure")
706 .contains("Invalid asset_type"));
707 }
708
709 #[test]
710 fn test_tool_hf_search_defaults() {
711 let mut server = McpServer::new();
713 let req = make_request(
714 "tools/call",
715 serde_json::json!({
716 "name": "hf_search",
717 "arguments": {
718 "query": "tiny"
719 }
720 }),
721 );
722 let resp = server.handle_request(&req);
723 assert!(resp.result.is_some());
724 }
725
726 #[test]
731 fn test_tool_hf_info_dataset() {
732 let mut server = McpServer::new();
733 let req = make_request(
734 "tools/call",
735 serde_json::json!({
736 "name": "hf_info",
737 "arguments": {
738 "repo_id": "squad",
739 "asset_type": "dataset"
740 }
741 }),
742 );
743 let resp = server.handle_request(&req);
744 assert!(resp.result.is_some());
745 }
746
747 #[test]
748 fn test_tool_hf_info_space() {
749 let mut server = McpServer::new();
750 let req = make_request(
751 "tools/call",
752 serde_json::json!({
753 "name": "hf_info",
754 "arguments": {
755 "repo_id": "stabilityai/stable-diffusion",
756 "asset_type": "space"
757 }
758 }),
759 );
760 let resp = server.handle_request(&req);
761 assert!(resp.result.is_some());
762 }
763
764 #[test]
765 fn test_tool_hf_info_invalid_asset_type() {
766 let mut server = McpServer::new();
767 let req = make_request(
768 "tools/call",
769 serde_json::json!({
770 "name": "hf_info",
771 "arguments": {
772 "repo_id": "test/repo",
773 "asset_type": "invalid"
774 }
775 }),
776 );
777 let resp = server.handle_request(&req);
778 let result = resp.result.expect("operation failed");
779 assert_eq!(result["isError"], true);
780 assert!(result["content"][0]["text"]
781 .as_str()
782 .expect("unexpected failure")
783 .contains("Invalid asset_type"));
784 }
785
786 #[test]
787 fn test_tool_hf_info_default_asset_type() {
788 let mut server = McpServer::new();
790 let req = make_request(
791 "tools/call",
792 serde_json::json!({
793 "name": "hf_info",
794 "arguments": {
795 "repo_id": "meta-llama/Llama-2-7b-hf"
796 }
797 }),
798 );
799 let resp = server.handle_request(&req);
800 assert!(resp.result.is_some());
801 }
802
803 #[test]
808 fn test_tool_hf_tree_with_category() {
809 let mut server = McpServer::new();
810 let req = make_request(
811 "tools/call",
812 serde_json::json!({
813 "name": "hf_tree",
814 "arguments": {
815 "category": "inference"
816 }
817 }),
818 );
819 let resp = server.handle_request(&req);
820 assert!(resp.result.is_some());
821 let result = resp.result.expect("operation failed");
822 let content = result["content"].as_array().expect("expected array value");
823 assert!(content[0]["text"].as_str().expect("expected string value").contains("Inference"));
825 }
826
827 #[test]
832 fn test_tool_call_no_arguments_key() {
833 let mut server = McpServer::new();
834 let req = make_request(
835 "tools/call",
836 serde_json::json!({
837 "name": "hf_integration"
838 }),
839 );
840 let resp = server.handle_request(&req);
841 assert!(resp.result.is_some());
842 let result = resp.result.expect("operation failed");
843 let content = result["content"].as_array().expect("expected array value");
845 assert!(content[0]["text"].as_str().expect("expected string value").contains("PAIML"));
846 }
847
848 #[test]
853 fn test_initialize_response_serialization() {
854 let mut server = McpServer::new();
855 let req = make_request("initialize", serde_json::json!({}));
856 let resp = server.handle_request(&req);
857 let json = serde_json::to_string(&resp).expect("json serialize failed");
858 assert!(json.contains("protocolVersion"));
859 assert!(json.contains("batuta-hf"));
860 assert!(json.contains("2.0"));
861 }
862
863 #[test]
864 fn test_tools_list_serialization() {
865 let mut server = McpServer::new();
866 let req = make_request("tools/list", serde_json::json!({}));
867 let resp = server.handle_request(&req);
868 let json = serde_json::to_string(&resp).expect("json serialize failed");
869 assert!(json.contains("hf_search"));
871 assert!(json.contains("hf_info"));
872 assert!(json.contains("hf_tree"));
873 assert!(json.contains("hf_integration"));
874 assert!(json.contains("inputSchema"));
875 }
876
877 #[test]
878 fn test_error_response_serialization() {
879 let mut server = McpServer::new();
880 let req = make_request("nonexistent", serde_json::json!({}));
881 let resp = server.handle_request(&req);
882 let json = serde_json::to_string(&resp).expect("json serialize failed");
883 assert!(json.contains("error"));
884 assert!(json.contains("-32601"));
885 assert!(json.contains("Method not found"));
886 }
887
888 #[test]
889 fn test_request_with_null_id() {
890 let mut server = McpServer::new();
891 let req = JsonRpcRequest {
892 jsonrpc: "2.0".to_string(),
893 id: None,
894 method: "initialize".to_string(),
895 params: serde_json::json!({}),
896 };
897 let resp = server.handle_request(&req);
898 assert!(resp.result.is_some());
899 assert!(resp.id.is_none());
900 }
901}