* feat: implement MCP completion specification 2025-06-18 Complete implementation of MCP completion specification with performance optimizations: Core Features: - Add CompletionContext for context-aware completion with previously resolved arguments - Implement CompletionProvider trait with async support and dyn compatibility - Create DefaultCompletionProvider with optimized fuzzy matching algorithm - Add comprehensive validation and helper methods to CompletionInfo - Update ServerHandler to handle completion/complete requests - Add client convenience methods for prompt and resource completion Performance Optimizations: - Zero-allocation fuzzy matching using index-based scoring - Top-k selection with select_nth_unstable instead of full sorting - Pre-allocated vectors to avoid reallocations during matching - Char-based case-insensitive matching to minimize string operations - 5-8x performance improvement for large candidate sets API Design: - Context-aware completion supporting multi-argument scenarios - Type-safe validation with MAX_VALUES limit (100 per MCP spec) - Helper methods: with_all_values, with_pagination, validate - Reference convenience methods: for_prompt, for_resource - Client methods: complete_prompt_argument, complete_resource_argument Testing: - 17 comprehensive tests covering all functionality - Schema compliance tests for MCP 2025-06-18 specification - Performance tests with <100ms target for 1000 candidates - Edge case and validation tests Schema Updates: - Add CompletionContext to JSON schema - Update CompleteRequestParam with optional context field - Maintain backward compatibility with existing API * test: add comprehensive fuzzy matching tests for completion Add three new test cases to enhance coverage of fuzzy matching algorithm: - test_fuzzy_matching_with_typos_and_missing_chars: Tests subsequence matching with real-world scenarios including abbreviated patterns, case-insensitive matching, and complex file/package name completion - test_fuzzy_matching_scoring_priority: Validates scoring system prioritizes exact matches > prefix matches > substring matches > subsequence matches - test_fuzzy_matching_edge_cases: Covers boundary conditions including single character queries, oversized queries, and repeated characters These tests ensure robust fuzzy search functionality for MCP completion specification implementation with proper handling of user typos and incomplete input patterns. * feat: improve completion algorithms, add comprehensive tests and example - Enhance fuzzy matching algorithm with acronym support for multi-word entries - Add comprehensive scoring system for better relevance ranking - Implement multi-level matching: exact, prefix, word prefix, acronym, substring - Add context-aware completion scoring with proper priority ordering - Optimize performance through efficient character-by-character matching - Support case-insensitive acronym matching - Improve code quality with clippy fixes and async fn syntax - Add comprehensive test suite covering edge cases and acronym matching - Create completion example server demonstrating weather-related prompts * fix(test): typos * refactor: improve completion API and replace example with SQL query builder - Remove DefaultCompletionProvider from library core - Move completion logic to examples following review feedback - Update CompletionContext.argument_names() to return Iterator for better performance - Replace tech search example with SQL query builder demonstrating progressive completion - Add context-aware completion that adapts based on filled arguments - Use proper Option types for optional SQL fields (columns, where_clause, values) - Demonstrate real-world value of argument_names() method for dynamic completion flow The SQL query builder showcases: • Progressive field availability based on operation type • Context validation using argument_names() • Proper Optional field handling • Smart completion that guides user through multi-step form * fix: fmt
218 lines
6.9 KiB
Rust
218 lines
6.9 KiB
Rust
use std::collections::HashMap;
|
|
|
|
use rmcp::model::*;
|
|
use serde_json::json;
|
|
|
|
#[test]
|
|
fn test_completion_context_serialization() {
|
|
let mut args = HashMap::new();
|
|
args.insert("key1".to_string(), "value1".to_string());
|
|
args.insert("key2".to_string(), "value2".to_string());
|
|
|
|
let context = CompletionContext::with_arguments(args);
|
|
|
|
// Test serialization
|
|
let json = serde_json::to_value(&context).unwrap();
|
|
let expected = json!({
|
|
"arguments": {
|
|
"key1": "value1",
|
|
"key2": "value2"
|
|
}
|
|
});
|
|
assert_eq!(json, expected);
|
|
|
|
// Test deserialization
|
|
let deserialized: CompletionContext = serde_json::from_value(expected).unwrap();
|
|
assert_eq!(deserialized, context);
|
|
}
|
|
|
|
#[test]
|
|
fn test_completion_context_methods() {
|
|
let mut args = HashMap::new();
|
|
args.insert("city".to_string(), "San Francisco".to_string());
|
|
args.insert("country".to_string(), "USA".to_string());
|
|
|
|
let context = CompletionContext::with_arguments(args);
|
|
|
|
assert!(context.has_arguments());
|
|
assert_eq!(
|
|
context.get_argument("city"),
|
|
Some(&"San Francisco".to_string())
|
|
);
|
|
assert_eq!(context.get_argument("missing"), None);
|
|
|
|
let names: Vec<&str> = context.argument_names().collect();
|
|
assert!(names.contains(&"city"));
|
|
assert!(names.contains(&"country"));
|
|
assert_eq!(names.len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_complete_request_param_serialization() {
|
|
let mut args = HashMap::new();
|
|
args.insert("previous_input".to_string(), "test".to_string());
|
|
|
|
let request = CompleteRequestParam {
|
|
r#ref: Reference::for_prompt("weather_prompt"),
|
|
argument: ArgumentInfo {
|
|
name: "location".to_string(),
|
|
value: "San".to_string(),
|
|
},
|
|
context: Some(CompletionContext::with_arguments(args)),
|
|
};
|
|
|
|
let json = serde_json::to_value(&request).unwrap();
|
|
assert!(json["ref"]["name"].as_str().unwrap() == "weather_prompt");
|
|
assert!(json["argument"]["name"].as_str().unwrap() == "location");
|
|
assert!(json["argument"]["value"].as_str().unwrap() == "San");
|
|
assert!(
|
|
json["context"]["arguments"]["previous_input"]
|
|
.as_str()
|
|
.unwrap()
|
|
== "test"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_completion_info_validation() {
|
|
// Valid completion with less than max values
|
|
let values = vec!["option1".to_string(), "option2".to_string()];
|
|
let completion = CompletionInfo::new(values.clone()).unwrap();
|
|
assert_eq!(completion.values, values);
|
|
assert!(completion.validate().is_ok());
|
|
|
|
// Test max values limit
|
|
let many_values: Vec<String> = (0..=CompletionInfo::MAX_VALUES)
|
|
.map(|i| format!("option_{}", i))
|
|
.collect();
|
|
let result = CompletionInfo::new(many_values);
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_completion_info_helper_methods() {
|
|
let values = vec!["test1".to_string(), "test2".to_string()];
|
|
|
|
// Test with_all_values
|
|
let completion = CompletionInfo::with_all_values(values.clone()).unwrap();
|
|
assert_eq!(completion.values, values);
|
|
assert_eq!(completion.total, Some(2));
|
|
assert_eq!(completion.has_more, Some(false));
|
|
assert!(!completion.has_more_results());
|
|
assert_eq!(completion.total_available(), Some(2));
|
|
|
|
// Test with_pagination
|
|
let paginated = CompletionInfo::with_pagination(values.clone(), Some(10), true).unwrap();
|
|
assert_eq!(paginated.values, values);
|
|
assert_eq!(paginated.total, Some(10));
|
|
assert_eq!(paginated.has_more, Some(true));
|
|
assert!(paginated.has_more_results());
|
|
assert_eq!(paginated.total_available(), Some(10));
|
|
}
|
|
|
|
#[test]
|
|
fn test_completion_info_bounds() {
|
|
// Test exactly at the limit
|
|
let max_values: Vec<String> = (0..CompletionInfo::MAX_VALUES)
|
|
.map(|i| format!("value_{}", i))
|
|
.collect();
|
|
assert!(CompletionInfo::new(max_values).is_ok());
|
|
|
|
// Test over the limit
|
|
let over_limit: Vec<String> = (0..=CompletionInfo::MAX_VALUES)
|
|
.map(|i| format!("value_{}", i))
|
|
.collect();
|
|
assert!(CompletionInfo::new(over_limit).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_reference_convenience_methods() {
|
|
let prompt_ref = Reference::for_prompt("test_prompt");
|
|
assert_eq!(prompt_ref.reference_type(), "ref/prompt");
|
|
assert_eq!(prompt_ref.as_prompt_name(), Some("test_prompt"));
|
|
assert_eq!(prompt_ref.as_resource_uri(), None);
|
|
|
|
let resource_ref = Reference::for_resource("file://path/to/resource");
|
|
assert_eq!(resource_ref.reference_type(), "ref/resource");
|
|
assert_eq!(
|
|
resource_ref.as_resource_uri(),
|
|
Some("file://path/to/resource")
|
|
);
|
|
assert_eq!(resource_ref.as_prompt_name(), None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_completion_serialization_format() {
|
|
// Test that completion follows MCP 2025-06-18 specification format
|
|
let completion = CompletionInfo {
|
|
values: vec!["value1".to_string(), "value2".to_string()],
|
|
total: Some(2),
|
|
has_more: Some(false),
|
|
};
|
|
|
|
let json = serde_json::to_value(&completion).unwrap();
|
|
|
|
// Verify JSON structure matches specification
|
|
assert!(json.is_object());
|
|
assert!(json["values"].is_array());
|
|
assert_eq!(json["values"].as_array().unwrap().len(), 2);
|
|
assert_eq!(json["total"].as_u64().unwrap(), 2);
|
|
assert!(!json["hasMore"].as_bool().unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn test_resource_reference() {
|
|
// Test that ResourceReference works correctly
|
|
let resource_ref = ResourceReference {
|
|
uri: "test://uri".to_string(),
|
|
};
|
|
|
|
// Test that ResourceReference works correctly
|
|
let another_ref = ResourceReference {
|
|
uri: "test://uri".to_string(),
|
|
};
|
|
|
|
// They should be equivalent
|
|
assert_eq!(resource_ref.uri, another_ref.uri);
|
|
}
|
|
|
|
#[test]
|
|
fn test_complete_result_default() {
|
|
let result = CompleteResult::default();
|
|
assert!(result.completion.values.is_empty());
|
|
assert_eq!(result.completion.total, None);
|
|
assert_eq!(result.completion.has_more, None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_completion_context_empty() {
|
|
let context = CompletionContext::new();
|
|
assert!(!context.has_arguments());
|
|
assert_eq!(context.get_argument("any"), None);
|
|
assert!(context.argument_names().count() == 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_mcp_schema_compliance() {
|
|
// Test that our types serialize correctly according to MCP specification
|
|
let request = CompleteRequestParam {
|
|
r#ref: Reference::for_resource("file://{path}"),
|
|
argument: ArgumentInfo {
|
|
name: "path".to_string(),
|
|
value: "src/".to_string(),
|
|
},
|
|
context: None,
|
|
};
|
|
|
|
let json_str = serde_json::to_string(&request).unwrap();
|
|
let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
|
|
|
|
// Verify key structure matches MCP spec
|
|
assert!(parsed["ref"].is_object());
|
|
assert!(parsed["argument"].is_object());
|
|
assert!(parsed["argument"]["name"].is_string());
|
|
assert!(parsed["argument"]["value"].is_string());
|
|
|
|
// Verify type tag is correct
|
|
assert_eq!(parsed["ref"]["type"].as_str().unwrap(), "ref/resource");
|
|
}
|