mcpkit_server/capability/
completions.rs1use crate::context::Context;
7use crate::handler::CompletionHandler;
8use mcpkit_core::error::McpError;
9use mcpkit_core::types::completion::{CompleteRequest, CompleteResult, Completion, CompletionRef};
10use std::collections::HashMap;
11use std::future::Future;
12use std::pin::Pin;
13
14pub type BoxedCompletionFn = Box<
16 dyn for<'a> Fn(
17 &'a str,
18 &'a Context<'a>,
19 )
20 -> Pin<Box<dyn Future<Output = Result<Vec<String>, McpError>> + Send + 'a>>
21 + Send
22 + Sync,
23>;
24
25pub struct RegisteredCompletion {
27 pub ref_type: String,
29 pub ref_value: String,
31 pub arg_name: String,
33 pub handler: BoxedCompletionFn,
35}
36
37pub struct CompletionService {
41 completions: HashMap<(String, String, String), RegisteredCompletion>,
43}
44
45impl Default for CompletionService {
46 fn default() -> Self {
47 Self::new()
48 }
49}
50
51impl CompletionService {
52 #[must_use]
54 pub fn new() -> Self {
55 Self {
56 completions: HashMap::new(),
57 }
58 }
59
60 pub fn register_prompt_completion<F, Fut>(
62 &mut self,
63 prompt_name: impl Into<String>,
64 arg_name: impl Into<String>,
65 handler: F,
66 ) where
67 F: Fn(&str, &Context<'_>) -> Fut + Send + Sync + 'static,
68 Fut: Future<Output = Result<Vec<String>, McpError>> + Send + 'static,
69 {
70 let ref_type = "ref/prompt".to_string();
71 let ref_value = prompt_name.into();
72 let arg_name = arg_name.into();
73 let key = (ref_type.clone(), ref_value.clone(), arg_name.clone());
74
75 let boxed: BoxedCompletionFn = Box::new(move |input, ctx| Box::pin(handler(input, ctx)));
76
77 self.completions.insert(
78 key,
79 RegisteredCompletion {
80 ref_type,
81 ref_value,
82 arg_name,
83 handler: boxed,
84 },
85 );
86 }
87
88 pub fn register_resource_completion<F, Fut>(
90 &mut self,
91 uri_pattern: impl Into<String>,
92 arg_name: impl Into<String>,
93 handler: F,
94 ) where
95 F: Fn(&str, &Context<'_>) -> Fut + Send + Sync + 'static,
96 Fut: Future<Output = Result<Vec<String>, McpError>> + Send + 'static,
97 {
98 let ref_type = "ref/resource".to_string();
99 let ref_value = uri_pattern.into();
100 let arg_name = arg_name.into();
101 let key = (ref_type.clone(), ref_value.clone(), arg_name.clone());
102
103 let boxed: BoxedCompletionFn = Box::new(move |input, ctx| Box::pin(handler(input, ctx)));
104
105 self.completions.insert(
106 key,
107 RegisteredCompletion {
108 ref_type,
109 ref_value,
110 arg_name,
111 handler: boxed,
112 },
113 );
114 }
115
116 #[must_use]
118 pub fn has_completion(&self, ref_type: &str, ref_value: &str, arg_name: &str) -> bool {
119 let key = (
120 ref_type.to_string(),
121 ref_value.to_string(),
122 arg_name.to_string(),
123 );
124 self.completions.contains_key(&key)
125 }
126}
127
128impl CompletionHandler for CompletionService {
129 async fn complete(
130 &self,
131 request: &CompleteRequest,
132 ctx: &Context<'_>,
133 ) -> Result<CompleteResult, McpError> {
134 let key = (
139 request.ref_.ref_type().to_string(),
140 request.ref_.value().to_string(),
141 request.argument.name.clone(),
142 );
143
144 let Some(registered) = self.completions.get(&key) else {
145 return Ok(Completion::new(Vec::new()).into());
146 };
147 let values = (registered.handler)(&request.argument.value, ctx).await?;
148 Ok(Completion::new(values).into())
149 }
150}
151
152pub struct CompleteRequestBuilder {
154 ref_: CompletionRef,
155 arg_name: String,
156 arg_value: String,
157}
158
159impl CompleteRequestBuilder {
160 pub fn for_prompt(prompt_name: impl Into<String>, arg_name: impl Into<String>) -> Self {
162 Self {
163 ref_: CompletionRef::prompt(prompt_name.into()),
164 arg_name: arg_name.into(),
165 arg_value: String::new(),
166 }
167 }
168
169 pub fn for_resource(uri: impl Into<String>, arg_name: impl Into<String>) -> Self {
171 Self {
172 ref_: CompletionRef::resource(uri.into()),
173 arg_name: arg_name.into(),
174 arg_value: String::new(),
175 }
176 }
177
178 pub fn value(mut self, value: impl Into<String>) -> Self {
180 self.arg_value = value.into();
181 self
182 }
183
184 #[must_use]
186 pub fn build(self) -> CompleteRequest {
187 CompleteRequest {
188 ref_: self.ref_,
189 argument: mcpkit_core::types::completion::CompletionArgument {
190 name: self.arg_name,
191 value: self.arg_value,
192 },
193 context: None,
194 meta: None,
195 }
196 }
197}
198
199pub struct CompletionFilter;
201
202impl CompletionFilter {
203 #[must_use]
205 pub fn by_prefix(values: &[String], prefix: &str) -> Vec<String> {
206 let prefix_lower = prefix.to_lowercase();
207 values
208 .iter()
209 .filter(|v| v.to_lowercase().starts_with(&prefix_lower))
210 .cloned()
211 .collect()
212 }
213
214 #[must_use]
216 pub fn by_substring(values: &[String], substring: &str) -> Vec<String> {
217 let sub_lower = substring.to_lowercase();
218 values
219 .iter()
220 .filter(|v| v.to_lowercase().contains(&sub_lower))
221 .cloned()
222 .collect()
223 }
224
225 #[must_use]
227 pub fn limit(values: Vec<String>, max: usize) -> Vec<String> {
228 values.into_iter().take(max).collect()
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use crate::context::{Context, NoOpPeer};
236 use mcpkit_core::capability::{ClientCapabilities, ServerCapabilities};
237 use mcpkit_core::protocol::RequestId;
238 use mcpkit_core::protocol_version::ProtocolVersion;
239
240 fn make_context() -> (
241 RequestId,
242 ClientCapabilities,
243 ServerCapabilities,
244 ProtocolVersion,
245 NoOpPeer,
246 ) {
247 (
248 RequestId::Number(1),
249 ClientCapabilities::default(),
250 ServerCapabilities::default(),
251 ProtocolVersion::LATEST,
252 NoOpPeer,
253 )
254 }
255
256 #[test]
257 fn test_complete_request_builder() {
258 let request = CompleteRequestBuilder::for_prompt("code-review", "language")
259 .value("py")
260 .build();
261
262 assert_eq!(request.ref_.ref_type(), "ref/prompt");
263 assert_eq!(request.argument.name, "language");
264 assert_eq!(request.argument.value, "py");
265 }
266
267 #[test]
268 fn test_completion_filter() {
269 let values = vec![
270 "python".to_string(),
271 "javascript".to_string(),
272 "typescript".to_string(),
273 "rust".to_string(),
274 ];
275
276 let filtered = CompletionFilter::by_prefix(&values, "py");
277 assert_eq!(filtered, vec!["python"]);
278
279 let filtered = CompletionFilter::by_substring(&values, "script");
280 assert_eq!(filtered, vec!["javascript", "typescript"]);
281
282 let limited = CompletionFilter::limit(values, 2);
283 assert_eq!(limited.len(), 2);
284 }
285
286 #[tokio::test]
287 async fn test_completion_service() -> Result<(), Box<dyn std::error::Error>> {
288 let mut service = CompletionService::new();
289
290 let languages = vec![
291 "python".to_string(),
292 "javascript".to_string(),
293 "typescript".to_string(),
294 "rust".to_string(),
295 ];
296
297 service.register_prompt_completion("code-review", "language", move |input, _ctx| {
298 let langs = languages.clone();
299 let input = input.to_string();
300 async move { Ok(CompletionFilter::by_prefix(&langs, &input)) }
301 });
302
303 assert!(service.has_completion("ref/prompt", "code-review", "language"));
304
305 let (req_id, client_caps, server_caps, protocol_version, peer) = make_context();
306 let ctx = Context::new(
307 &req_id,
308 None,
309 &client_caps,
310 &server_caps,
311 protocol_version,
312 &peer,
313 );
314
315 let request = CompleteRequestBuilder::for_prompt("code-review", "language")
316 .value("py")
317 .build();
318
319 let result = service.complete(&request, &ctx).await?;
320 assert_eq!(result.completion.values, vec!["python"]);
321
322 Ok(())
323 }
324}