Skip to main content

turbovault_plugin_api/
completion.rs

1use std::collections::BTreeMap;
2
3/// What a completion request is asking a plugin to complete.
4///
5/// Both variants name the plugin-local identifier, not the published one: a
6/// plugin never spells its own namespace here any more than it does when
7/// declaring a tool.
8#[derive(Debug, Clone, PartialEq, Eq)]
9#[non_exhaustive]
10pub enum CompletionTarget {
11    /// An argument of a prompt, by local name.
12    Prompt(String),
13    /// An expression in a resource template, by local URI template.
14    ResourceTemplate(String),
15}
16
17/// A request to suggest values for an argument a user is part-way through.
18///
19/// Completion is what makes a resource template usable: without it a client can
20/// only show `note/{path}` and hope the user types a real path. It is called
21/// while the user types, so answer from state already in memory rather than
22/// scanning the vault on each keystroke.
23#[derive(Debug, Clone, PartialEq, Eq)]
24#[non_exhaustive]
25pub struct CompletionRequest {
26    /// The prompt or resource template being filled in.
27    pub target: CompletionTarget,
28    /// Name of the argument or template expression being completed.
29    pub argument: String,
30    /// What the user has typed so far. Often empty on the first request.
31    pub value: String,
32    /// Arguments of the same target the client has already resolved.
33    ///
34    /// A template with more than one expression can use these to narrow later
35    /// suggestions to what is reachable given the earlier choices.
36    pub resolved: BTreeMap<String, String>,
37}
38
39impl CompletionRequest {
40    /// Construct a request for `argument` with the partial input `value`.
41    pub fn new(
42        target: CompletionTarget,
43        argument: impl Into<String>,
44        value: impl Into<String>,
45    ) -> Self {
46        Self {
47            target,
48            argument: argument.into(),
49            value: value.into(),
50            resolved: BTreeMap::new(),
51        }
52    }
53
54    /// Attach the arguments the client has already resolved.
55    pub fn with_resolved(mut self, resolved: BTreeMap<String, String>) -> Self {
56        self.resolved = resolved;
57        self
58    }
59}
60
61/// Suggested values for one argument.
62///
63/// The host truncates [`Self::values`] to the limit MCP places on a single
64/// completion response and marks the result as having more, so a plugin may
65/// return everything it knows without tracking the protocol's cap.
66#[derive(Debug, Clone, Default, PartialEq, Eq)]
67#[non_exhaustive]
68pub struct Completion {
69    /// Suggestions, best first.
70    pub values: Vec<String>,
71    /// How many exist in total, when the plugin knows and it exceeds what it
72    /// returned.
73    pub total: Option<u32>,
74    /// Whether more exist beyond [`Self::values`].
75    ///
76    /// Leave unset unless the plugin itself truncated; the host sets it when
77    /// its own truncation drops values.
78    pub has_more: Option<bool>,
79}
80
81impl Completion {
82    /// Suggest nothing. A valid answer, and the default.
83    pub fn none() -> Self {
84        Self::default()
85    }
86
87    /// Suggest `values`, best first.
88    pub fn new(values: impl IntoIterator<Item = String>) -> Self {
89        Self {
90            values: values.into_iter().collect(),
91            ..Self::default()
92        }
93    }
94
95    /// Report how many suggestions exist in total.
96    pub fn with_total(mut self, total: u32) -> Self {
97        self.total = Some(total);
98        self
99    }
100
101    /// Report that the plugin itself returned only part of what it knows.
102    pub fn with_has_more(mut self, has_more: bool) -> Self {
103        self.has_more = Some(has_more);
104        self
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn completions_default_to_suggesting_nothing() {
114        let empty = Completion::none();
115        assert!(empty.values.is_empty());
116        assert_eq!(empty.total, None);
117        assert_eq!(empty.has_more, None);
118        assert_eq!(empty, Completion::default());
119    }
120
121    #[test]
122    fn requests_carry_previously_resolved_arguments() {
123        let request = CompletionRequest::new(
124            CompletionTarget::ResourceTemplate("note/{folder}/{name}".to_string()),
125            "name",
126            "dai",
127        )
128        .with_resolved(BTreeMap::from([(
129            "folder".to_string(),
130            "journal".to_string(),
131        )]));
132        assert_eq!(
133            request.resolved.get("folder").map(String::as_str),
134            Some("journal")
135        );
136        assert_eq!(request.value, "dai");
137    }
138}