1use crate::core::{
16 completion::{CompletionContext, CompletionHandler},
17 error::{McpError, McpResult},
18};
19use crate::protocol::messages::{CompletionArgument, CompletionReference};
20use async_trait::async_trait;
21use std::collections::HashMap;
22use std::path::{Path, PathBuf};
23use tokio::fs;
24
25pub struct FileSystemCompletionHandler {
45 base_path: PathBuf,
47 allowed_extensions: Option<Vec<String>>,
49 include_hidden: bool,
51 max_suggestions: usize,
53 max_depth: usize,
55}
56
57impl FileSystemCompletionHandler {
58 pub fn new<P: AsRef<Path>>(base_path: P) -> Self {
63 Self {
64 base_path: base_path.as_ref().to_path_buf(),
65 allowed_extensions: None,
66 include_hidden: false,
67 max_suggestions: 20,
68 max_depth: 5,
69 }
70 }
71
72 pub fn with_extensions(mut self, extensions: Vec<&str>) -> Self {
77 self.allowed_extensions = Some(extensions.into_iter().map(|s| s.to_string()).collect());
78 self
79 }
80
81 pub fn include_hidden_files(mut self, include: bool) -> Self {
83 self.include_hidden = include;
84 self
85 }
86
87 pub fn max_suggestions(mut self, max: usize) -> Self {
89 self.max_suggestions = max;
90 self
91 }
92
93 pub fn max_depth(mut self, depth: usize) -> Self {
95 self.max_depth = depth;
96 self
97 }
98
99 fn should_include_file(&self, path: &Path) -> bool {
101 if let Some(ref extensions) = self.allowed_extensions {
102 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
103 return extensions.contains(&ext.to_string());
104 }
105 return false;
106 }
107 true
108 }
109
110 fn should_include_hidden(&self, path: &Path) -> bool {
112 if !self.include_hidden {
113 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
114 return !name.starts_with('.');
115 }
116 }
117 true
118 }
119
120 async fn get_path_completions(&self, prefix: &str) -> McpResult<Vec<String>> {
122 let mut suggestions = Vec::new();
123
124 let search_path = if prefix.is_empty() {
126 self.base_path.clone()
127 } else {
128 let prefix_path = Path::new(prefix);
129 if prefix_path.is_absolute() {
130 prefix_path.to_path_buf()
131 } else {
132 self.base_path.join(prefix_path)
133 }
134 };
135
136 let (dir_to_search, partial_name) = if search_path.exists() && search_path.is_dir() {
138 (search_path, String::new())
139 } else {
140 let parent = search_path.parent().unwrap_or(&self.base_path);
141 let partial = search_path
142 .file_name()
143 .and_then(|n| n.to_str())
144 .unwrap_or("")
145 .to_string();
146 (parent.to_path_buf(), partial)
147 };
148
149 match fs::read_dir(&dir_to_search).await {
151 Ok(mut entries) => {
152 while let Some(entry) = entries.next_entry().await.map_err(McpError::io)? {
153 let path = entry.path();
154 let file_name = path
155 .file_name()
156 .and_then(|n| n.to_str())
157 .unwrap_or("")
158 .to_string();
159
160 if !partial_name.is_empty() && !file_name.starts_with(&partial_name) {
162 continue;
163 }
164
165 if !self.should_include_hidden(&path) {
167 continue;
168 }
169
170 if path.is_file() && !self.should_include_file(&path) {
172 continue;
173 }
174
175 let relative_path = path
177 .strip_prefix(&self.base_path)
178 .unwrap_or(&path)
179 .to_string_lossy()
180 .to_string();
181
182 let suggestion = if path.is_dir() {
184 format!("{relative_path}/")
185 } else {
186 relative_path
187 };
188
189 suggestions.push(suggestion);
190
191 if suggestions.len() >= self.max_suggestions {
192 break;
193 }
194 }
195 }
196 Err(e) => {
197 tracing::warn!("Failed to read directory {:?}: {}", dir_to_search, e);
198 return Ok(vec![]);
199 }
200 }
201
202 suggestions.sort();
204 Ok(suggestions)
205 }
206}
207
208#[async_trait]
209impl CompletionHandler for FileSystemCompletionHandler {
210 async fn complete(
211 &self,
212 reference: &CompletionReference,
213 argument: &CompletionArgument,
214 _context: Option<&CompletionContext>,
215 ) -> McpResult<Vec<String>> {
216 if let CompletionReference::Resource { uri } = reference {
217 if uri.starts_with("file://") {
219 let _path_part = uri.strip_prefix("file://").unwrap_or("");
220 return self.get_path_completions(&argument.value).await;
221 }
222
223 if argument.name == "path" || argument.name == "filename" || argument.name == "uri" {
225 return self.get_path_completions(&argument.value).await;
226 }
227 }
228
229 Ok(vec![])
230 }
231}
232
233pub struct FuzzyCompletionHandler {
253 options: Vec<String>,
255 threshold: f64,
257 max_suggestions: usize,
259 case_insensitive: bool,
261}
262
263impl FuzzyCompletionHandler {
264 pub fn new<S: AsRef<str>>(options: Vec<S>) -> Self {
269 Self {
270 options: options
271 .into_iter()
272 .map(|s| s.as_ref().to_string())
273 .collect(),
274 threshold: 0.4,
275 max_suggestions: 10,
276 case_insensitive: true,
277 }
278 }
279
280 pub fn threshold(mut self, threshold: f64) -> Self {
282 self.threshold = threshold.clamp(0.0, 1.0);
283 self
284 }
285
286 pub fn max_suggestions(mut self, max: usize) -> Self {
288 self.max_suggestions = max;
289 self
290 }
291
292 pub fn case_sensitive(mut self, sensitive: bool) -> Self {
294 self.case_insensitive = !sensitive;
295 self
296 }
297
298 fn similarity(&self, a: &str, b: &str) -> f64 {
300 let a = if self.case_insensitive {
301 a.to_lowercase()
302 } else {
303 a.to_string()
304 };
305 let b = if self.case_insensitive {
306 b.to_lowercase()
307 } else {
308 b.to_string()
309 };
310
311 if a == b {
312 return 1.0;
313 }
314
315 if a.is_empty() || b.is_empty() {
316 return 0.0;
317 }
318
319 if b.starts_with(&a) {
321 return 0.9 + (a.len() as f64 / b.len() as f64) * 0.1;
322 }
323
324 if b.contains(&a) {
326 return 0.7 + (a.len() as f64 / b.len() as f64) * 0.2;
327 }
328
329 let mut matches = 0;
331 let a_chars: Vec<char> = a.chars().collect();
332 let b_chars: Vec<char> = b.chars().collect();
333
334 for ac in &a_chars {
335 if b_chars.contains(ac) {
336 matches += 1;
337 }
338 }
339
340 matches as f64 / a_chars.len().max(b_chars.len()) as f64
341 }
342
343 fn get_fuzzy_completions(&self, input: &str) -> Vec<String> {
345 let mut scored_options: Vec<(f64, String)> = self
346 .options
347 .iter()
348 .map(|option| {
349 let score = self.similarity(input, option);
350 (score, option.clone())
351 })
352 .filter(|(score, _)| *score >= self.threshold)
353 .collect();
354
355 scored_options.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
357
358 scored_options
360 .into_iter()
361 .take(self.max_suggestions)
362 .map(|(_, option)| option)
363 .collect()
364 }
365}
366
367#[async_trait]
368impl CompletionHandler for FuzzyCompletionHandler {
369 async fn complete(
370 &self,
371 _reference: &CompletionReference,
372 argument: &CompletionArgument,
373 _context: Option<&CompletionContext>,
374 ) -> McpResult<Vec<String>> {
375 Ok(self.get_fuzzy_completions(&argument.value))
376 }
377}
378
379pub struct SchemaCompletionHandler {
408 schema: serde_json::Value,
410 custom_completions: HashMap<String, Vec<String>>,
412}
413
414impl SchemaCompletionHandler {
415 pub fn new(schema: serde_json::Value) -> Self {
420 Self {
421 schema,
422 custom_completions: HashMap::new(),
423 }
424 }
425
426 pub fn add_custom_completions<S: AsRef<str>>(
432 mut self,
433 parameter_name: S,
434 values: Vec<S>,
435 ) -> Self {
436 let values: Vec<String> = values.into_iter().map(|s| s.as_ref().to_string()).collect();
437 self.custom_completions
438 .insert(parameter_name.as_ref().to_string(), values);
439 self
440 }
441
442 fn get_enum_values(&self, property: &serde_json::Value) -> Vec<String> {
444 if let Some(enum_array) = property.get("enum").and_then(|e| e.as_array()) {
445 return enum_array
446 .iter()
447 .filter_map(|v| v.as_str())
448 .map(|s| s.to_string())
449 .collect();
450 }
451 vec![]
452 }
453
454 fn get_format_suggestions(&self, format: &str, current_value: &str) -> Vec<String> {
456 match format {
457 "email" => {
458 if current_value.is_empty() {
459 vec!["user@example.com".to_string()]
460 } else if !current_value.contains('@') {
461 vec![format!("{}@example.com", current_value)]
462 } else {
463 vec![]
464 }
465 }
466 "date" => {
467 if current_value.is_empty() {
468 vec!["2025-01-01".to_string()]
469 } else {
470 vec![]
471 }
472 }
473 "time" => {
474 if current_value.is_empty() {
475 vec!["12:00:00".to_string()]
476 } else {
477 vec![]
478 }
479 }
480 "uri" => {
481 if current_value.is_empty() {
482 vec!["https://example.com".to_string()]
483 } else if !current_value.contains("://") {
484 vec![
485 format!("https://{}", current_value),
486 format!("http://{}", current_value),
487 format!("file://{}", current_value),
488 ]
489 } else {
490 vec![]
491 }
492 }
493 _ => vec![],
494 }
495 }
496
497 fn get_parameter_completions(&self, parameter_name: &str, current_value: &str) -> Vec<String> {
499 if let Some(custom) = self.custom_completions.get(parameter_name) {
501 return custom
502 .iter()
503 .filter(|value| value.starts_with(current_value))
504 .cloned()
505 .collect();
506 }
507
508 if let Some(properties) = self.schema.get("properties").and_then(|p| p.as_object()) {
510 if let Some(property) = properties.get(parameter_name) {
511 let enum_values = self.get_enum_values(property);
513 if !enum_values.is_empty() {
514 return enum_values
515 .into_iter()
516 .filter(|value| value.starts_with(current_value))
517 .collect();
518 }
519
520 if let Some(format) = property.get("format").and_then(|f| f.as_str()) {
522 return self.get_format_suggestions(format, current_value);
523 }
524
525 if let Some(type_str) = property.get("type").and_then(|t| t.as_str()) {
527 match type_str {
528 "boolean" => {
529 return vec!["true".to_string(), "false".to_string()]
530 .into_iter()
531 .filter(|value| value.starts_with(current_value))
532 .collect();
533 }
534 "number" | "integer" if current_value.is_empty() => {
535 return vec!["0".to_string(), "1".to_string(), "10".to_string()];
536 }
537 _ => {}
538 }
539 }
540 }
541 }
542
543 vec![]
544 }
545}
546
547#[async_trait]
548impl CompletionHandler for SchemaCompletionHandler {
549 async fn complete(
550 &self,
551 _reference: &CompletionReference,
552 argument: &CompletionArgument,
553 _context: Option<&CompletionContext>,
554 ) -> McpResult<Vec<String>> {
555 Ok(self.get_parameter_completions(&argument.name, &argument.value))
556 }
557}
558
559pub struct CompositeCompletionHandler {
575 handlers: HashMap<String, Box<dyn CompletionHandler>>,
577 default_handler: Option<Box<dyn CompletionHandler>>,
579}
580
581impl CompositeCompletionHandler {
582 pub fn new() -> Self {
584 Self {
585 handlers: HashMap::new(),
586 default_handler: None,
587 }
588 }
589
590 pub fn add_handler<H>(mut self, name: &str, handler: H) -> Self
596 where
597 H: CompletionHandler + 'static,
598 {
599 self.handlers.insert(name.to_string(), Box::new(handler));
600 self
601 }
602
603 pub fn with_default<H>(mut self, handler: H) -> Self
608 where
609 H: CompletionHandler + 'static,
610 {
611 self.default_handler = Some(Box::new(handler));
612 self
613 }
614
615 fn select_handler(
617 &self,
618 reference: &CompletionReference,
619 argument: &CompletionArgument,
620 ) -> Option<&dyn CompletionHandler> {
621 let handler_key = match reference {
623 CompletionReference::Prompt { .. } => {
624 if argument.name == "name" {
625 Some("prompts".to_string())
626 } else {
627 Some(format!("prompt_{}", argument.name))
628 }
629 }
630 CompletionReference::Resource { .. } => {
631 if argument.name == "uri" || argument.name == "path" {
632 Some("files".to_string())
633 } else {
634 Some(format!("resource_{}", argument.name))
635 }
636 }
637 CompletionReference::Tool { name } => Some(format!("tool_{}_{}", name, argument.name)),
638 };
639
640 if let Some(key) = handler_key {
642 if let Some(handler) = self.handlers.get(&key) {
643 return Some(handler.as_ref());
644 }
645 }
646
647 match reference {
649 CompletionReference::Prompt { .. } => self.handlers.get("prompts").map(|h| h.as_ref()),
650 CompletionReference::Resource { .. } => {
651 self.handlers.get("resources").map(|h| h.as_ref())
652 }
653 CompletionReference::Tool { .. } => self.handlers.get("tools").map(|h| h.as_ref()),
654 }
655 .or_else(|| self.default_handler.as_ref().map(|h| h.as_ref()))
656 }
657}
658
659impl Default for CompositeCompletionHandler {
660 fn default() -> Self {
661 Self::new()
662 }
663}
664
665#[async_trait]
666impl CompletionHandler for CompositeCompletionHandler {
667 async fn complete(
668 &self,
669 reference: &CompletionReference,
670 argument: &CompletionArgument,
671 context: Option<&CompletionContext>,
672 ) -> McpResult<Vec<String>> {
673 if let Some(handler) = self.select_handler(reference, argument) {
674 handler.complete(reference, argument, context).await
675 } else {
676 Ok(vec![])
677 }
678 }
679}
680
681#[cfg(test)]
682mod tests {
683 use super::*;
684 use tempfile::TempDir;
685 use tokio::fs::File;
686
687 #[tokio::test]
688 async fn test_filesystem_completion() {
689 let temp_dir = TempDir::new().unwrap();
690 let temp_path = temp_dir.path();
691
692 File::create(temp_path.join("test.txt")).await.unwrap();
694 File::create(temp_path.join("example.md")).await.unwrap();
695 tokio::fs::create_dir(temp_path.join("subdir"))
696 .await
697 .unwrap();
698
699 let handler =
700 FileSystemCompletionHandler::new(temp_path).with_extensions(vec!["txt", "md"]);
701
702 let reference = CompletionReference::Resource {
703 uri: "file:///test".to_string(),
704 };
705 let argument = CompletionArgument {
706 name: "path".to_string(),
707 value: "".to_string(),
708 };
709
710 let completions = handler.complete(&reference, &argument, None).await.unwrap();
711
712 assert!(completions.len() >= 2);
714 assert!(completions.iter().any(|c| c.contains("test.txt")));
715 assert!(completions.iter().any(|c| c.contains("example.md")));
716 }
717
718 #[tokio::test]
719 async fn test_fuzzy_completion() {
720 let handler = FuzzyCompletionHandler::new(vec![
721 "analyze_data",
722 "analyze_text",
723 "create_report",
724 "generate_summary",
725 ])
726 .threshold(0.3);
727
728 let reference = CompletionReference::Prompt {
729 name: "test".to_string(),
730 };
731 let argument = CompletionArgument {
732 name: "name".to_string(),
733 value: "ana".to_string(),
734 };
735
736 let completions = handler.complete(&reference, &argument, None).await.unwrap();
737
738 assert_eq!(completions.len(), 2);
740 assert!(completions.contains(&"analyze_data".to_string()));
741 assert!(completions.contains(&"analyze_text".to_string()));
742 }
743
744 #[tokio::test]
745 async fn test_schema_completion() {
746 let schema = serde_json::json!({
747 "type": "object",
748 "properties": {
749 "priority": {
750 "type": "string",
751 "enum": ["low", "medium", "high"]
752 },
753 "email": {
754 "type": "string",
755 "format": "email"
756 }
757 }
758 });
759
760 let handler = SchemaCompletionHandler::new(schema);
761
762 let reference = CompletionReference::Tool {
763 name: "create_task".to_string(),
764 };
765
766 let argument = CompletionArgument {
768 name: "priority".to_string(),
769 value: "m".to_string(),
770 };
771
772 let completions = handler.complete(&reference, &argument, None).await.unwrap();
773 assert_eq!(completions, vec!["medium".to_string()]);
774
775 let argument = CompletionArgument {
777 name: "email".to_string(),
778 value: "user".to_string(),
779 };
780
781 let completions = handler.complete(&reference, &argument, None).await.unwrap();
782 assert_eq!(completions, vec!["user@example.com".to_string()]);
783 }
784
785 #[tokio::test]
786 async fn test_composite_completion() {
787 let fuzzy = FuzzyCompletionHandler::new(vec!["prompt1", "prompt2"]);
788 let schema = SchemaCompletionHandler::new(serde_json::json!({
789 "type": "object",
790 "properties": {
791 "status": {
792 "type": "string",
793 "enum": ["active", "inactive"]
794 }
795 }
796 }));
797
798 let composite = CompositeCompletionHandler::new()
799 .add_handler("prompts", fuzzy)
800 .add_handler("tool_create_task_status", schema);
801
802 let reference = CompletionReference::Prompt {
804 name: "test".to_string(),
805 };
806 let argument = CompletionArgument {
807 name: "name".to_string(),
808 value: "prom".to_string(),
809 };
810
811 let completions = composite
812 .complete(&reference, &argument, None)
813 .await
814 .unwrap();
815 assert!(completions.contains(&"prompt1".to_string()));
816
817 let reference = CompletionReference::Tool {
819 name: "create_task".to_string(),
820 };
821 let argument = CompletionArgument {
822 name: "status".to_string(),
823 value: "a".to_string(),
824 };
825
826 let completions = composite
827 .complete(&reference, &argument, None)
828 .await
829 .unwrap();
830 assert_eq!(completions, vec!["active".to_string()]);
831 }
832}