Skip to main content

mcpkit_server/capability/
completions.rs

1//! Completion capability implementation.
2//!
3//! This module provides support for argument completion
4//! in MCP servers.
5
6use 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
14/// A boxed async function for handling completion requests.
15pub 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
25/// A registered completion provider.
26pub struct RegisteredCompletion {
27    /// Reference type (prompt or resource).
28    pub ref_type: String,
29    /// Reference value (name or URI pattern).
30    pub ref_value: String,
31    /// Argument name.
32    pub arg_name: String,
33    /// Completion handler.
34    pub handler: BoxedCompletionFn,
35}
36
37/// Service for handling completion requests.
38///
39/// This provides argument completion for prompts and resources.
40pub struct CompletionService {
41    /// Completions keyed by (`ref_type`, `ref_value`, `arg_name`).
42    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    /// Create a new completion service.
53    #[must_use]
54    pub fn new() -> Self {
55        Self {
56            completions: HashMap::new(),
57        }
58    }
59
60    /// Register a completion provider for a prompt argument.
61    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    /// Register a completion provider for a resource argument.
89    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    /// Check if a completion provider exists.
117    #[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        // Dispatch to the registered closure for this (ref, argument). Closures
135        // return the matching values; `total`/`has_more` are derived here (a
136        // handler that needs a real superset total or paging should implement
137        // `CompletionHandler` directly rather than via the closure registry).
138        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
152/// Builder for creating completion requests.
153pub struct CompleteRequestBuilder {
154    ref_: CompletionRef,
155    arg_name: String,
156    arg_value: String,
157}
158
159impl CompleteRequestBuilder {
160    /// Create a builder for prompt argument completion.
161    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    /// Create a builder for resource argument completion.
170    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    /// Set the current input value.
179    pub fn value(mut self, value: impl Into<String>) -> Self {
180        self.arg_value = value.into();
181        self
182    }
183
184    /// Build the request.
185    #[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
199/// Helper for filtering completions.
200pub struct CompletionFilter;
201
202impl CompletionFilter {
203    /// Filter completions by prefix match.
204    #[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    /// Filter completions by substring match.
215    #[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    /// Filter and limit completions.
226    #[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}