Skip to content

Commit 452fe2c

Browse files
authored
feat: implement context-aware completion (MCP 2025-06-18) (#396)
* 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
1 parent b482cfc commit 452fe2c

File tree

7 files changed

+961
-13
lines changed

7 files changed

+961
-13
lines changed

crates/rmcp/src/handler/server.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ pub trait ServerHandler: Sized + Send + Sync + 'static {
122122
request: CompleteRequestParam,
123123
context: RequestContext<RoleServer>,
124124
) -> impl Future<Output = Result<CompleteResult, McpError>> + Send + '_ {
125-
std::future::ready(Err(McpError::method_not_found::<CompleteRequestMethod>()))
125+
std::future::ready(Ok(CompleteResult::default()))
126126
}
127127
fn set_level(
128128
&self,

crates/rmcp/src/model.rs

Lines changed: 154 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1070,17 +1070,66 @@ pub struct ModelHint {
10701070
// COMPLETION AND AUTOCOMPLETE
10711071
// =============================================================================
10721072

1073+
/// Context for completion requests providing previously resolved arguments.
1074+
///
1075+
/// This enables context-aware completion where subsequent argument completions
1076+
/// can take into account the values of previously resolved arguments.
1077+
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
1078+
#[serde(rename_all = "camelCase")]
1079+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1080+
pub struct CompletionContext {
1081+
/// Previously resolved argument values that can inform completion suggestions
1082+
#[serde(skip_serializing_if = "Option::is_none")]
1083+
pub arguments: Option<std::collections::HashMap<String, String>>,
1084+
}
1085+
1086+
impl CompletionContext {
1087+
/// Create a new empty completion context
1088+
pub fn new() -> Self {
1089+
Self::default()
1090+
}
1091+
1092+
/// Create a completion context with the given arguments
1093+
pub fn with_arguments(arguments: std::collections::HashMap<String, String>) -> Self {
1094+
Self {
1095+
arguments: Some(arguments),
1096+
}
1097+
}
1098+
1099+
/// Get a specific argument value by name
1100+
pub fn get_argument(&self, name: &str) -> Option<&String> {
1101+
self.arguments.as_ref()?.get(name)
1102+
}
1103+
1104+
/// Check if the context has any arguments
1105+
pub fn has_arguments(&self) -> bool {
1106+
self.arguments.as_ref().is_some_and(|args| !args.is_empty())
1107+
}
1108+
1109+
/// Get all argument names
1110+
pub fn argument_names(&self) -> impl Iterator<Item = &str> {
1111+
self.arguments
1112+
.as_ref()
1113+
.into_iter()
1114+
.flat_map(|args| args.keys())
1115+
.map(|k| k.as_str())
1116+
}
1117+
}
1118+
10731119
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
10741120
#[serde(rename_all = "camelCase")]
10751121
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
10761122
pub struct CompleteRequestParam {
10771123
pub r#ref: Reference,
10781124
pub argument: ArgumentInfo,
1125+
/// Optional context containing previously resolved argument values
1126+
#[serde(skip_serializing_if = "Option::is_none")]
1127+
pub context: Option<CompletionContext>,
10791128
}
10801129

10811130
pub type CompleteRequest = Request<CompleteRequestMethod, CompleteRequestParam>;
10821131

1083-
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
1132+
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
10841133
#[serde(rename_all = "camelCase")]
10851134
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
10861135
pub struct CompletionInfo {
@@ -1091,7 +1140,74 @@ pub struct CompletionInfo {
10911140
pub has_more: Option<bool>,
10921141
}
10931142

1094-
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
1143+
impl CompletionInfo {
1144+
/// Maximum number of completion values allowed per response according to MCP specification
1145+
pub const MAX_VALUES: usize = 100;
1146+
1147+
/// Create a new CompletionInfo with validation for maximum values
1148+
pub fn new(values: Vec<String>) -> Result<Self, String> {
1149+
if values.len() > Self::MAX_VALUES {
1150+
return Err(format!(
1151+
"Too many completion values: {} (max: {})",
1152+
values.len(),
1153+
Self::MAX_VALUES
1154+
));
1155+
}
1156+
Ok(Self {
1157+
values,
1158+
total: None,
1159+
has_more: None,
1160+
})
1161+
}
1162+
1163+
/// Create CompletionInfo with all values and no pagination
1164+
pub fn with_all_values(values: Vec<String>) -> Result<Self, String> {
1165+
let completion = Self::new(values)?;
1166+
Ok(Self {
1167+
total: Some(completion.values.len() as u32),
1168+
has_more: Some(false),
1169+
..completion
1170+
})
1171+
}
1172+
1173+
/// Create CompletionInfo with pagination information
1174+
pub fn with_pagination(
1175+
values: Vec<String>,
1176+
total: Option<u32>,
1177+
has_more: bool,
1178+
) -> Result<Self, String> {
1179+
let completion = Self::new(values)?;
1180+
Ok(Self {
1181+
total,
1182+
has_more: Some(has_more),
1183+
..completion
1184+
})
1185+
}
1186+
1187+
/// Check if this completion response indicates more results are available
1188+
pub fn has_more_results(&self) -> bool {
1189+
self.has_more.unwrap_or(false)
1190+
}
1191+
1192+
/// Get the total number of available completions, if known
1193+
pub fn total_available(&self) -> Option<u32> {
1194+
self.total
1195+
}
1196+
1197+
/// Validate that the completion info complies with MCP specification
1198+
pub fn validate(&self) -> Result<(), String> {
1199+
if self.values.len() > Self::MAX_VALUES {
1200+
return Err(format!(
1201+
"Too many completion values: {} (max: {})",
1202+
self.values.len(),
1203+
Self::MAX_VALUES
1204+
));
1205+
}
1206+
Ok(())
1207+
}
1208+
}
1209+
1210+
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
10951211
#[serde(rename_all = "camelCase")]
10961212
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
10971213
pub struct CompleteResult {
@@ -1108,6 +1224,42 @@ pub enum Reference {
11081224
Prompt(PromptReference),
11091225
}
11101226

1227+
impl Reference {
1228+
/// Create a prompt reference
1229+
pub fn for_prompt(name: impl Into<String>) -> Self {
1230+
Self::Prompt(PromptReference { name: name.into() })
1231+
}
1232+
1233+
/// Create a resource reference
1234+
pub fn for_resource(uri: impl Into<String>) -> Self {
1235+
Self::Resource(ResourceReference { uri: uri.into() })
1236+
}
1237+
1238+
/// Get the reference type as a string
1239+
pub fn reference_type(&self) -> &'static str {
1240+
match self {
1241+
Self::Prompt(_) => "ref/prompt",
1242+
Self::Resource(_) => "ref/resource",
1243+
}
1244+
}
1245+
1246+
/// Extract prompt name if this is a prompt reference
1247+
pub fn as_prompt_name(&self) -> Option<&str> {
1248+
match self {
1249+
Self::Prompt(prompt_ref) => Some(&prompt_ref.name),
1250+
_ => None,
1251+
}
1252+
}
1253+
1254+
/// Extract resource URI if this is a resource reference
1255+
pub fn as_resource_uri(&self) -> Option<&str> {
1256+
match self {
1257+
Self::Resource(resource_ref) => Some(&resource_ref.uri),
1258+
_ => None,
1259+
}
1260+
}
1261+
}
1262+
11111263
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
11121264
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
11131265
pub struct ResourceReference {

crates/rmcp/src/service/client.rs

Lines changed: 102 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,18 @@ use thiserror::Error;
55
use super::*;
66
use crate::{
77
model::{
8-
CallToolRequest, CallToolRequestParam, CallToolResult, CancelledNotification,
8+
ArgumentInfo, CallToolRequest, CallToolRequestParam, CallToolResult, CancelledNotification,
99
CancelledNotificationParam, ClientInfo, ClientJsonRpcMessage, ClientNotification,
1010
ClientRequest, ClientResult, CompleteRequest, CompleteRequestParam, CompleteResult,
11-
GetPromptRequest, GetPromptRequestParam, GetPromptResult, InitializeRequest,
12-
InitializedNotification, JsonRpcResponse, ListPromptsRequest, ListPromptsResult,
13-
ListResourceTemplatesRequest, ListResourceTemplatesResult, ListResourcesRequest,
14-
ListResourcesResult, ListToolsRequest, ListToolsResult, PaginatedRequestParam,
15-
ProgressNotification, ProgressNotificationParam, ReadResourceRequest,
16-
ReadResourceRequestParam, ReadResourceResult, RequestId, RootsListChangedNotification,
17-
ServerInfo, ServerJsonRpcMessage, ServerNotification, ServerRequest, ServerResult,
18-
SetLevelRequest, SetLevelRequestParam, SubscribeRequest, SubscribeRequestParam,
19-
UnsubscribeRequest, UnsubscribeRequestParam,
11+
CompletionContext, CompletionInfo, GetPromptRequest, GetPromptRequestParam,
12+
GetPromptResult, InitializeRequest, InitializedNotification, JsonRpcResponse,
13+
ListPromptsRequest, ListPromptsResult, ListResourceTemplatesRequest,
14+
ListResourceTemplatesResult, ListResourcesRequest, ListResourcesResult, ListToolsRequest,
15+
ListToolsResult, PaginatedRequestParam, ProgressNotification, ProgressNotificationParam,
16+
ReadResourceRequest, ReadResourceRequestParam, ReadResourceResult, Reference, RequestId,
17+
RootsListChangedNotification, ServerInfo, ServerJsonRpcMessage, ServerNotification,
18+
ServerRequest, ServerResult, SetLevelRequest, SetLevelRequestParam, SubscribeRequest,
19+
SubscribeRequestParam, UnsubscribeRequest, UnsubscribeRequestParam,
2020
},
2121
transport::DynamicTransportError,
2222
};
@@ -390,4 +390,96 @@ impl Peer<RoleClient> {
390390
}
391391
Ok(resource_templates)
392392
}
393+
394+
/// Convenient method to get completion suggestions for a prompt argument
395+
///
396+
/// # Arguments
397+
/// * `prompt_name` - Name of the prompt being completed
398+
/// * `argument_name` - Name of the argument being completed
399+
/// * `current_value` - Current partial value of the argument
400+
/// * `context` - Optional context with previously resolved arguments
401+
///
402+
/// # Returns
403+
/// CompletionInfo with suggestions for the specified prompt argument
404+
pub async fn complete_prompt_argument(
405+
&self,
406+
prompt_name: impl Into<String>,
407+
argument_name: impl Into<String>,
408+
current_value: impl Into<String>,
409+
context: Option<CompletionContext>,
410+
) -> Result<CompletionInfo, ServiceError> {
411+
let request = CompleteRequestParam {
412+
r#ref: Reference::for_prompt(prompt_name),
413+
argument: ArgumentInfo {
414+
name: argument_name.into(),
415+
value: current_value.into(),
416+
},
417+
context,
418+
};
419+
420+
let result = self.complete(request).await?;
421+
Ok(result.completion)
422+
}
423+
424+
/// Convenient method to get completion suggestions for a resource URI argument
425+
///
426+
/// # Arguments
427+
/// * `uri_template` - URI template pattern being completed
428+
/// * `argument_name` - Name of the URI parameter being completed
429+
/// * `current_value` - Current partial value of the parameter
430+
/// * `context` - Optional context with previously resolved arguments
431+
///
432+
/// # Returns
433+
/// CompletionInfo with suggestions for the specified resource URI argument
434+
pub async fn complete_resource_argument(
435+
&self,
436+
uri_template: impl Into<String>,
437+
argument_name: impl Into<String>,
438+
current_value: impl Into<String>,
439+
context: Option<CompletionContext>,
440+
) -> Result<CompletionInfo, ServiceError> {
441+
let request = CompleteRequestParam {
442+
r#ref: Reference::for_resource(uri_template),
443+
argument: ArgumentInfo {
444+
name: argument_name.into(),
445+
value: current_value.into(),
446+
},
447+
context,
448+
};
449+
450+
let result = self.complete(request).await?;
451+
Ok(result.completion)
452+
}
453+
454+
/// Simple completion for a prompt argument without context
455+
///
456+
/// This is a convenience wrapper around `complete_prompt_argument` for
457+
/// simple completion scenarios that don't require context awareness.
458+
pub async fn complete_prompt_simple(
459+
&self,
460+
prompt_name: impl Into<String>,
461+
argument_name: impl Into<String>,
462+
current_value: impl Into<String>,
463+
) -> Result<Vec<String>, ServiceError> {
464+
let completion = self
465+
.complete_prompt_argument(prompt_name, argument_name, current_value, None)
466+
.await?;
467+
Ok(completion.values)
468+
}
469+
470+
/// Simple completion for a resource URI argument without context
471+
///
472+
/// This is a convenience wrapper around `complete_resource_argument` for
473+
/// simple completion scenarios that don't require context awareness.
474+
pub async fn complete_resource_simple(
475+
&self,
476+
uri_template: impl Into<String>,
477+
argument_name: impl Into<String>,
478+
current_value: impl Into<String>,
479+
) -> Result<Vec<String>, ServiceError> {
480+
let completion = self
481+
.complete_resource_argument(uri_template, argument_name, current_value, None)
482+
.await?;
483+
Ok(completion.values)
484+
}
393485
}

0 commit comments

Comments
 (0)