Skip to main content

batuta/mcp/
server.rs

1//! MCP Server Implementation
2//!
3//! Handles JSON-RPC 2.0 messages over stdio for the HuggingFace MCP tools.
4
5use super::types::*;
6use crate::hf::hub_client::{HubAssetType, HubClient, SearchFilters};
7use std::collections::HashMap;
8
9/// MCP Server for batuta HuggingFace tools
10pub struct McpServer {
11    hub_client: HubClient,
12}
13
14impl McpServer {
15    /// Create a new MCP server
16    pub fn new() -> Self {
17        Self { hub_client: HubClient::new() }
18    }
19
20    /// Handle a JSON-RPC request and return a response
21    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    /// Handle initialize request
35    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    /// Handle tools/list request
52    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    /// Handle tools/call request
58    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    // ========================================================================
78    // Tool Definitions
79    // ========================================================================
80
81    /// Return all available tool definitions
82    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    // ========================================================================
192    // Tool Implementations
193    // ========================================================================
194
195    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    /// Run the MCP server on stdio (blocking)
323    #[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    // =====================================================================
543    // Coverage: tool definition schema validation
544    // =====================================================================
545
546    #[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        // Verify properties
557        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        // Verify enum values for asset_type
564        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        // Verify non-enum properties have None enum
571        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        // Verify property types
576        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        // Verify asset_type enum
595        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    // =====================================================================
626    // Coverage: search with task filter and dataset/space types
627    // =====================================================================
628
629    #[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        // May succeed or error depending on network, but should not panic
648        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        // No asset_type or limit specified -- defaults to "model" and 10
712        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    // =====================================================================
727    // Coverage: hf_info with dataset/space and invalid asset_type
728    // =====================================================================
729
730    #[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        // No asset_type: defaults to "model"
789        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    // =====================================================================
804    // Coverage: hf_tree with category filter
805    // =====================================================================
806
807    #[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        // Tree output should contain the ecosystem structure
824        assert!(content[0]["text"].as_str().expect("expected string value").contains("Inference"));
825    }
826
827    // =====================================================================
828    // Coverage: tools/call with no arguments key
829    // =====================================================================
830
831    #[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        // Should use default empty object for arguments
844        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    // =====================================================================
849    // Coverage: JSON-RPC serialization
850    // =====================================================================
851
852    #[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        // Verify all tools appear in serialized JSON
870        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}