Skip to main content

pmcp/types/
completable.rs

1//! Completable arguments support for MCP.
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::collections::HashMap;
6
7/// Represents a completable argument that can provide suggestions.
8///
9/// # Examples
10///
11/// ```rust
12/// use pmcp::types::completable::{CompletableArgument, CompletionConfig, CompletionProvider};
13/// use std::collections::HashMap;
14/// use serde_json::json;
15///
16/// // Simple required argument
17/// let name_arg = CompletableArgument {
18///     name: "name".to_string(),
19///     description: Some("The name of the resource".to_string()),
20///     required: true,
21///     completion: None,
22/// };
23///
24/// // Argument with static completion
25/// let status_arg = CompletableArgument {
26///     name: "status".to_string(),
27///     description: Some("Status to filter by".to_string()),
28///     required: false,
29///     completion: Some(CompletionConfig {
30///         provider: CompletionProvider::Static,
31///         config: {
32///             let mut config = HashMap::new();
33///             config.insert("values".to_string(),
34///                 json!(["active", "inactive", "pending"]));
35///             config
36///         },
37///     }),
38/// };
39///
40/// // File path completion argument
41/// let file_arg = CompletableArgument {
42///     name: "file_path".to_string(),
43///     description: Some("Path to the file".to_string()),
44///     required: true,
45///     completion: Some(CompletionConfig {
46///         provider: CompletionProvider::File,
47///         config: {
48///             let mut config = HashMap::new();
49///             config.insert("extensions".to_string(),
50///                 json!([".txt", ".md", ".json"]));
51///             config.insert("basePath".to_string(),
52///                 json!("/home/user/documents"));
53///             config
54///         },
55///     }),
56/// };
57/// ```
58#[derive(Debug, Clone, Serialize, Deserialize)]
59#[serde(rename_all = "camelCase")]
60pub struct CompletableArgument {
61    /// The argument name.
62    pub name: String,
63
64    /// Human-readable description.
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub description: Option<String>,
67
68    /// Whether this argument is required.
69    #[serde(default)]
70    pub required: bool,
71
72    /// Completion provider configuration.
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub completion: Option<CompletionConfig>,
75}
76
77/// Configuration for argument completion.
78#[derive(Debug, Clone, Serialize, Deserialize)]
79#[serde(rename_all = "camelCase")]
80pub struct CompletionConfig {
81    /// Type of completion provider.
82    pub provider: CompletionProvider,
83
84    /// Additional provider-specific configuration.
85    #[serde(flatten)]
86    pub config: HashMap<String, Value>,
87}
88
89/// Types of completion providers.
90#[derive(Debug, Clone, Serialize, Deserialize)]
91#[serde(rename_all = "camelCase")]
92pub enum CompletionProvider {
93    /// Static list of values.
94    Static,
95    /// Dynamic completion from a resource.
96    Resource,
97    /// Completion from a tool call.
98    Tool,
99    /// File path completion.
100    File,
101    /// Custom provider.
102    Custom(String),
103}
104
105/// Completion request for getting suggestions.
106#[derive(Debug, Clone, Serialize, Deserialize)]
107#[serde(rename_all = "camelCase")]
108pub struct CompletionRequest {
109    /// The argument being completed.
110    pub argument: String,
111
112    /// Current partial value.
113    pub partial: String,
114
115    /// Values of other arguments.
116    pub context: HashMap<String, String>,
117}
118
119/// Completion response with suggestions.
120#[derive(Debug, Clone, Serialize, Deserialize)]
121#[serde(rename_all = "camelCase")]
122pub struct CompletionResponse {
123    /// List of completion suggestions.
124    pub completions: Vec<CompletionItem>,
125
126    /// Whether more completions are available.
127    #[serde(default)]
128    pub has_more: bool,
129
130    /// Continuation token for pagination.
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub continuation_token: Option<String>,
133}
134
135/// A single completion suggestion.
136#[derive(Debug, Clone, Serialize, Deserialize)]
137#[serde(rename_all = "camelCase")]
138pub struct CompletionItem {
139    /// The value to insert.
140    pub value: String,
141
142    /// Display label (if different from value).
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub label: Option<String>,
145
146    /// Description of the completion.
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub description: Option<String>,
149
150    /// Icon or emoji for the completion.
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub icon: Option<String>,
153
154    /// Additional metadata.
155    #[serde(flatten)]
156    pub metadata: HashMap<String, Value>,
157}
158
159/// Trait for types that can provide completions.
160#[async_trait::async_trait]
161pub trait CompletionProviderTrait: Send + Sync {
162    /// Get completions for an argument.
163    async fn complete(
164        &self,
165        request: CompletionRequest,
166    ) -> crate::error::Result<CompletionResponse>;
167}
168
169/// Static completion provider with a fixed list of values.
170///
171/// # Examples
172///
173/// ```rust
174/// use pmcp::types::completable::{StaticCompletionProvider, CompletionItem, CompletionRequest, CompletionProviderTrait};
175/// use std::collections::HashMap;
176///
177/// # async fn example() -> Result<(), pmcp::Error> {
178/// // Create from strings
179/// let colors = StaticCompletionProvider::from_strings(vec![
180///     "red".to_string(),
181///     "green".to_string(),
182///     "blue".to_string(),
183/// ]);
184///
185/// // Create with detailed items
186/// let languages = StaticCompletionProvider::new(vec![
187///     CompletionItem {
188///         value: "rust".to_string(),
189///         label: Some("Rust".to_string()),
190///         description: Some("A systems programming language".to_string()),
191///         icon: Some("🦀".to_string()),
192///         metadata: HashMap::new(),
193///     },
194///     CompletionItem {
195///         value: "python".to_string(),
196///         label: Some("Python".to_string()),
197///         description: Some("A high-level programming language".to_string()),
198///         icon: Some("🐍".to_string()),
199///         metadata: HashMap::new(),
200///     },
201/// ]);
202///
203/// // Use the completion provider
204/// let request = CompletionRequest {
205///     argument: "language".to_string(),
206///     partial: "r".to_string(),
207///     context: HashMap::new(),
208/// };
209///
210/// let response = languages.complete(request).await?;
211/// assert!(!response.completions.is_empty());
212/// # Ok(())
213/// # }
214/// ```
215#[derive(Debug)]
216pub struct StaticCompletionProvider {
217    /// List of completion items.
218    items: Vec<CompletionItem>,
219}
220
221impl StaticCompletionProvider {
222    /// Create a new static completion provider.
223    pub fn new(items: Vec<CompletionItem>) -> Self {
224        Self { items }
225    }
226
227    /// Create from a list of strings.
228    pub fn from_strings(values: Vec<String>) -> Self {
229        Self {
230            items: values
231                .into_iter()
232                .map(|value| CompletionItem {
233                    value,
234                    label: None,
235                    description: None,
236                    icon: None,
237                    metadata: HashMap::new(),
238                })
239                .collect(),
240        }
241    }
242}
243
244#[async_trait::async_trait]
245impl CompletionProviderTrait for StaticCompletionProvider {
246    async fn complete(
247        &self,
248        request: CompletionRequest,
249    ) -> crate::error::Result<CompletionResponse> {
250        let completions: Vec<CompletionItem> = self
251            .items
252            .iter()
253            .filter(|item| {
254                item.value.starts_with(&request.partial)
255                    || item
256                        .label
257                        .as_ref()
258                        .is_some_and(|l| l.starts_with(&request.partial))
259            })
260            .cloned()
261            .collect();
262
263        Ok(CompletionResponse {
264            completions,
265            has_more: false,
266            continuation_token: None,
267        })
268    }
269}
270
271/// File path completion provider.
272#[derive(Debug)]
273pub struct FileCompletionProvider {
274    /// Base directory for relative paths.
275    base_dir: Option<std::path::PathBuf>,
276    /// File extensions to filter by.
277    extensions: Option<Vec<String>>,
278}
279
280impl FileCompletionProvider {
281    /// Create a new file completion provider.
282    pub fn new() -> Self {
283        Self {
284            base_dir: None,
285            extensions: None,
286        }
287    }
288
289    /// Set the base directory.
290    pub fn with_base_dir(mut self, dir: std::path::PathBuf) -> Self {
291        self.base_dir = Some(dir);
292        self
293    }
294
295    /// Set file extensions to filter by.
296    pub fn with_extensions(mut self, extensions: Vec<String>) -> Self {
297        self.extensions = Some(extensions);
298        self
299    }
300}
301
302#[async_trait::async_trait]
303impl CompletionProviderTrait for FileCompletionProvider {
304    async fn complete(
305        &self,
306        request: CompletionRequest,
307    ) -> crate::error::Result<CompletionResponse> {
308        use std::path::Path;
309
310        let partial_path = Path::new(&request.partial);
311        let (dir_path, file_prefix) = if request.partial.ends_with(std::path::MAIN_SEPARATOR) {
312            (partial_path, "")
313        } else {
314            (
315                partial_path.parent().unwrap_or_else(|| Path::new(".")),
316                partial_path
317                    .file_name()
318                    .and_then(|s| s.to_str())
319                    .unwrap_or(""),
320            )
321        };
322
323        let search_dir = if dir_path.is_absolute() {
324            dir_path.to_path_buf()
325        } else if let Some(base) = &self.base_dir {
326            base.join(dir_path)
327        } else {
328            dir_path.to_path_buf()
329        };
330
331        let mut completions = Vec::new();
332
333        if let Ok(entries) = std::fs::read_dir(&search_dir) {
334            for entry in entries.flatten() {
335                if let Some(name) = entry.file_name().to_str() {
336                    if name.starts_with(file_prefix) {
337                        let is_dir = entry.file_type().is_ok_and(|t| t.is_dir());
338
339                        // Check extension filter for files
340                        if !is_dir {
341                            if let Some(exts) = &self.extensions {
342                                let has_valid_ext = Path::new(name)
343                                    .extension()
344                                    .and_then(|e| e.to_str())
345                                    .is_some_and(|e| exts.iter().any(|ext| ext == e));
346
347                                if !has_valid_ext {
348                                    continue;
349                                }
350                            }
351                        }
352
353                        let value = if request.partial.is_empty() {
354                            name.to_string()
355                        } else {
356                            dir_path.join(name).to_string_lossy().to_string()
357                        };
358
359                        completions.push(CompletionItem {
360                            value,
361                            label: Some(name.to_string()),
362                            description: if is_dir {
363                                Some("Directory".to_string())
364                            } else {
365                                Some("File".to_string())
366                            },
367                            icon: if is_dir {
368                                Some("📁".to_string())
369                            } else {
370                                Some("📄".to_string())
371                            },
372                            metadata: HashMap::new(),
373                        });
374                    }
375                }
376            }
377        }
378
379        // Sort completions
380        completions.sort_by(|a, b| a.value.cmp(&b.value));
381
382        Ok(CompletionResponse {
383            completions,
384            has_more: false,
385            continuation_token: None,
386        })
387    }
388}
389
390impl Default for FileCompletionProvider {
391    fn default() -> Self {
392        Self::new()
393    }
394}
395
396/// Builder for creating completable arguments.
397#[derive(Debug)]
398pub struct CompletableBuilder {
399    name: String,
400    description: Option<String>,
401    required: bool,
402    completion: Option<CompletionConfig>,
403}
404
405impl CompletableBuilder {
406    /// Create a new completable argument builder.
407    pub fn new(name: impl Into<String>) -> Self {
408        Self {
409            name: name.into(),
410            description: None,
411            required: false,
412            completion: None,
413        }
414    }
415
416    /// Set the description.
417    pub fn description(mut self, desc: impl Into<String>) -> Self {
418        self.description = Some(desc.into());
419        self
420    }
421
422    /// Mark as required.
423    pub fn required(mut self) -> Self {
424        self.required = true;
425        self
426    }
427
428    /// Add static completions.
429    pub fn static_completions(mut self, values: Vec<String>) -> Self {
430        self.completion = Some(CompletionConfig {
431            provider: CompletionProvider::Static,
432            config: {
433                let mut config = HashMap::new();
434                config.insert("values".to_string(), serde_json::to_value(values).unwrap());
435                config
436            },
437        });
438        self
439    }
440
441    /// Add file path completions.
442    pub fn file_completions(mut self) -> Self {
443        self.completion = Some(CompletionConfig {
444            provider: CompletionProvider::File,
445            config: HashMap::new(),
446        });
447        self
448    }
449
450    /// Build the completable argument.
451    pub fn build(self) -> CompletableArgument {
452        CompletableArgument {
453            name: self.name,
454            description: self.description,
455            required: self.required,
456            completion: self.completion,
457        }
458    }
459}
460
461/// Helper function to create a completable argument.
462pub fn completable(name: impl Into<String>) -> CompletableBuilder {
463    CompletableBuilder::new(name)
464}
465
466#[cfg(test)]
467mod tests {
468    use super::*;
469
470    #[tokio::test]
471    async fn test_static_completion() {
472        let provider = StaticCompletionProvider::from_strings(vec![
473            "apple".to_string(),
474            "apricot".to_string(),
475            "banana".to_string(),
476            "cherry".to_string(),
477        ]);
478
479        let request = CompletionRequest {
480            argument: "fruit".to_string(),
481            partial: "ap".to_string(),
482            context: HashMap::new(),
483        };
484
485        let response = provider.complete(request).await.unwrap();
486        assert_eq!(response.completions.len(), 2);
487        assert_eq!(response.completions[0].value, "apple");
488        assert_eq!(response.completions[1].value, "apricot");
489    }
490
491    #[test]
492    fn test_completable_builder() {
493        let arg = completable("environment")
494            .description("Target environment")
495            .required()
496            .static_completions(vec![
497                "development".to_string(),
498                "staging".to_string(),
499                "production".to_string(),
500            ])
501            .build();
502
503        assert_eq!(arg.name, "environment");
504        assert_eq!(arg.description, Some("Target environment".to_string()));
505        assert!(arg.required);
506        assert!(arg.completion.is_some());
507    }
508}