turul_mcp_protocol_2025_06_18/
completion.rs

1//! MCP Completion Protocol Types
2//!
3//! This module defines types for completion requests in MCP.
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use std::collections::HashMap;
8
9/// A reference to a resource or resource template definition (per MCP spec)
10#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub struct ResourceTemplateReference {
13    #[serde(rename = "type")]
14    pub ref_type: String, // "ref/resource"
15    /// The URI or URI template of the resource
16    #[serde(rename = "uri")]
17    pub uri: String,
18}
19
20/// Identifies a prompt (per MCP spec) - extends BaseMetadata
21#[derive(Debug, Clone, Serialize, Deserialize)]
22#[serde(rename_all = "camelCase")]
23pub struct PromptReference {
24    #[serde(rename = "type")]
25    pub ref_type: String, // "ref/prompt"
26    /// The name of the prompt
27    pub name: String,
28    /// Optional description from BaseMetadata
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub description: Option<String>,
31}
32
33/// Union type for completion references (per MCP spec)
34#[derive(Debug, Clone, Serialize, Deserialize)]
35#[serde(untagged)]
36pub enum CompletionReference {
37    ResourceTemplate(ResourceTemplateReference),
38    Prompt(PromptReference),
39}
40
41/// Completion context (per MCP spec)
42#[derive(Debug, Clone, Serialize, Deserialize)]
43#[serde(rename_all = "camelCase")]
44pub struct CompletionContext {
45    /// Arguments context
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub arguments: Option<HashMap<String, String>>,
48}
49
50/// Argument being completed (per MCP spec)
51#[derive(Debug, Clone, Serialize, Deserialize)]
52#[serde(rename_all = "camelCase")]
53pub struct CompleteArgument {
54    /// Name of the argument
55    pub name: String,
56    /// Current value being completed
57    pub value: String,
58}
59
60/// Completion request parameters (per MCP spec)
61#[derive(Debug, Clone, Serialize, Deserialize)]
62#[serde(rename_all = "camelCase")]
63pub struct CompleteParams {
64    /// Reference to the prompt or resource being completed
65    #[serde(rename = "ref")]
66    pub reference: CompletionReference,
67    /// The argument being completed
68    pub argument: CompleteArgument,
69    /// Optional context
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub context: Option<CompletionContext>,
72    /// Meta information (optional _meta field inside params)
73    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
74    pub meta: Option<HashMap<String, Value>>,
75}
76
77impl CompleteParams {
78    pub fn new(reference: CompletionReference, argument: CompleteArgument) -> Self {
79        Self {
80            reference,
81            argument,
82            context: None,
83            meta: None,
84        }
85    }
86
87    pub fn with_context(mut self, context: CompletionContext) -> Self {
88        self.context = Some(context);
89        self
90    }
91
92    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
93        self.meta = Some(meta);
94        self
95    }
96}
97
98/// Complete completion/complete request (matches TypeScript CompleteRequest interface)
99#[derive(Debug, Clone, Serialize, Deserialize)]
100#[serde(rename_all = "camelCase")]
101pub struct CompleteRequest {
102    /// Method name (always "completion/complete")
103    pub method: String,
104    /// Request parameters
105    pub params: CompleteParams,
106}
107
108impl CompleteRequest {
109    pub fn new(reference: CompletionReference, argument: CompleteArgument) -> Self {
110        Self {
111            method: "completion/complete".to_string(),
112            params: CompleteParams::new(reference, argument),
113        }
114    }
115
116    pub fn with_context(mut self, context: CompletionContext) -> Self {
117        self.params = self.params.with_context(context);
118        self
119    }
120
121    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
122        self.params = self.params.with_meta(meta);
123        self
124    }
125}
126
127
128/// Completion result (per MCP spec)
129#[derive(Debug, Clone, Serialize, Deserialize)]
130#[serde(rename_all = "camelCase")]
131pub struct CompletionResult {
132    /// The completion values
133    pub values: Vec<String>,
134    /// Optional total number of possible completions
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub total: Option<u32>,
137    /// Whether there are more completions available
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub has_more: Option<bool>,
140}
141
142/// Complete completion/complete response (per MCP spec)
143#[derive(Debug, Clone, Serialize, Deserialize)]
144#[serde(rename_all = "camelCase")]
145pub struct CompleteResult {
146    /// The completion result
147    pub completion: CompletionResult,
148    /// Meta information (follows MCP Result interface)
149    #[serde(
150        default,
151        skip_serializing_if = "Option::is_none",
152        alias = "_meta",
153        rename = "_meta"
154    )]
155    pub meta: Option<HashMap<String, Value>>,
156}
157
158impl CompletionResult {
159    pub fn new(values: Vec<String>) -> Self {
160        Self {
161            values,
162            total: None,
163            has_more: None,
164        }
165    }
166
167    pub fn with_total(mut self, total: u32) -> Self {
168        self.total = Some(total);
169        self
170    }
171
172    pub fn with_has_more(mut self, has_more: bool) -> Self {
173        self.has_more = Some(has_more);
174        self
175    }
176}
177
178impl CompleteResult {
179    pub fn new(completion: CompletionResult) -> Self {
180        Self {
181            completion,
182            meta: None,
183        }
184    }
185
186    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
187        self.meta = Some(meta);
188        self
189    }
190}
191
192/// Convenience constructors
193impl ResourceTemplateReference {
194    pub fn new(uri: impl Into<String>) -> Self {
195        Self {
196            ref_type: "ref/resource".to_string(),
197            uri: uri.into(),
198        }
199    }
200}
201
202impl PromptReference {
203    pub fn new(name: impl Into<String>) -> Self {
204        Self {
205            ref_type: "ref/prompt".to_string(),
206            name: name.into(),
207            description: None,
208        }
209    }
210
211    pub fn with_description(mut self, description: impl Into<String>) -> Self {
212        self.description = Some(description.into());
213        self
214    }
215}
216
217impl CompletionReference {
218    pub fn resource(uri: impl Into<String>) -> Self {
219        Self::ResourceTemplate(ResourceTemplateReference::new(uri))
220    }
221
222    pub fn prompt(name: impl Into<String>) -> Self {
223        Self::Prompt(PromptReference::new(name))
224    }
225}
226
227impl CompleteArgument {
228    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
229        Self {
230            name: name.into(),
231            value: value.into(),
232        }
233    }
234}
235
236impl CompletionContext {
237    pub fn new() -> Self {
238        Self {
239            arguments: None,
240        }
241    }
242
243    pub fn with_arguments(mut self, arguments: HashMap<String, String>) -> Self {
244        self.arguments = Some(arguments);
245        self
246    }
247}
248
249// Trait implementations for protocol compliance
250use crate::traits::*;
251
252impl Params for CompleteParams {}
253
254impl HasMetaParam for CompleteParams {
255    fn meta(&self) -> Option<&HashMap<String, Value>> {
256        self.meta.as_ref()
257    }
258}
259
260impl HasMethod for CompleteRequest {
261    fn method(&self) -> &str {
262        &self.method
263    }
264}
265
266impl HasParams for CompleteRequest {
267    fn params(&self) -> Option<&dyn Params> {
268        Some(&self.params)
269    }
270}
271
272impl HasData for CompleteResult {
273    fn data(&self) -> HashMap<String, Value> {
274        let mut data = HashMap::new();
275        data.insert("completion".to_string(), serde_json::to_value(&self.completion).unwrap_or(Value::Null));
276        data
277    }
278}
279
280impl HasMeta for CompleteResult {
281    fn meta(&self) -> Option<HashMap<String, Value>> {
282        self.meta.clone()
283    }
284}
285
286impl RpcResult for CompleteResult {}
287
288// ===========================================
289// === Fine-Grained Completion Traits ===
290// ===========================================
291
292/// Trait for completion metadata (method, reference type)
293pub trait HasCompletionMetadata {
294    /// The completion method name
295    fn method(&self) -> &str;
296    
297    /// The reference being completed (prompt or resource)
298    fn reference(&self) -> &CompletionReference;
299}
300
301/// Trait for completion context (argument, context)
302pub trait HasCompletionContext {
303    /// The argument being completed
304    fn argument(&self) -> &CompleteArgument;
305    
306    /// Optional completion context
307    fn context(&self) -> Option<&CompletionContext> {
308        None
309    }
310}
311
312/// Trait for completion validation and processing
313pub trait HasCompletionHandling {
314    /// Validate the completion request
315    fn validate_request(&self, _request: &CompleteRequest) -> Result<(), String> {
316        Ok(())
317    }
318    
319    /// Filter completion values based on current input
320    fn filter_completions(&self, values: Vec<String>, current_value: &str) -> Vec<String> {
321        // Default: simple prefix matching
322        values
323            .into_iter()
324            .filter(|v| v.to_lowercase().starts_with(&current_value.to_lowercase()))
325            .collect()
326    }
327}
328
329/// Composed completion definition trait (automatically implemented via blanket impl)
330pub trait CompletionDefinition: 
331    HasCompletionMetadata + 
332    HasCompletionContext + 
333    HasCompletionHandling 
334{
335    /// Convert this completion definition to a protocol CompleteRequest
336    fn to_complete_request(&self) -> CompleteRequest {
337        let mut request = CompleteRequest::new(
338            self.reference().clone(),
339            self.argument().clone()
340        );
341        if let Some(context) = self.context() {
342            request = request.with_context(context.clone());
343        }
344        request
345    }
346}
347
348// Blanket implementation: any type implementing the fine-grained traits automatically gets CompletionDefinition
349impl<T> CompletionDefinition for T 
350where 
351    T: HasCompletionMetadata + HasCompletionContext + HasCompletionHandling 
352{}
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357    use serde_json::json;
358
359    #[test]
360    fn test_resource_template_reference() {
361        let ref_obj = ResourceTemplateReference::new("file:///test/{name}.txt");
362        
363        assert_eq!(ref_obj.ref_type, "ref/resource");
364        assert_eq!(ref_obj.uri, "file:///test/{name}.txt");
365        
366        let json_value = serde_json::to_value(&ref_obj).unwrap();
367        assert_eq!(json_value["type"], "ref/resource");
368        assert_eq!(json_value["uri"], "file:///test/{name}.txt");
369    }
370
371    #[test]
372    fn test_prompt_reference() {
373        let ref_obj = PromptReference::new("test_prompt")
374            .with_description("A test prompt");
375        
376        assert_eq!(ref_obj.ref_type, "ref/prompt");
377        assert_eq!(ref_obj.name, "test_prompt");
378        assert_eq!(ref_obj.description, Some("A test prompt".to_string()));
379        
380        let json_value = serde_json::to_value(&ref_obj).unwrap();
381        assert_eq!(json_value["type"], "ref/prompt");
382        assert_eq!(json_value["name"], "test_prompt");
383        assert_eq!(json_value["description"], "A test prompt");
384    }
385
386    #[test]
387    fn test_completion_reference_union() {
388        let resource_ref = CompletionReference::resource("file:///test.txt");
389        let prompt_ref = CompletionReference::prompt("my_prompt");
390        
391        // Test serialization
392        let resource_json = serde_json::to_value(&resource_ref).unwrap();
393        let prompt_json = serde_json::to_value(&prompt_ref).unwrap();
394        
395        assert_eq!(resource_json["type"], "ref/resource");
396        assert_eq!(resource_json["uri"], "file:///test.txt");
397        
398        assert_eq!(prompt_json["type"], "ref/prompt");
399        assert_eq!(prompt_json["name"], "my_prompt");
400    }
401
402    #[test]
403    fn test_complete_request_matches_typescript_spec() {
404        // Test CompleteRequest matches: { method: string, params: { ref: ..., argument: ..., context?: ..., _meta?: ... } }
405        let mut meta = HashMap::new();
406        meta.insert("requestId".to_string(), json!("req-123"));
407        
408        let mut context_args = HashMap::new();
409        context_args.insert("userId".to_string(), "123".to_string());
410        
411        let context = CompletionContext::new().with_arguments(context_args);
412        
413        let request = CompleteRequest::new(
414            CompletionReference::prompt("test_prompt"),
415            CompleteArgument::new("arg_name", "partial_value")
416        )
417        .with_context(context)
418        .with_meta(meta);
419        
420        let json_value = serde_json::to_value(&request).unwrap();
421        
422        assert_eq!(json_value["method"], "completion/complete");
423        assert!(json_value["params"].is_object());
424        assert!(json_value["params"]["ref"].is_object());
425        assert_eq!(json_value["params"]["ref"]["type"], "ref/prompt");
426        assert_eq!(json_value["params"]["ref"]["name"], "test_prompt");
427        assert_eq!(json_value["params"]["argument"]["name"], "arg_name");
428        assert_eq!(json_value["params"]["argument"]["value"], "partial_value");
429        assert!(json_value["params"]["context"].is_object());
430        assert_eq!(json_value["params"]["context"]["arguments"]["userId"], "123");
431        assert_eq!(json_value["params"]["_meta"]["requestId"], "req-123");
432    }
433
434    #[test]
435    fn test_complete_result_matches_typescript_spec() {
436        // Test CompleteResult matches: { completion: { values: string[], total?: number, hasMore?: boolean }, _meta?: ... }
437        let mut meta = HashMap::new();
438        meta.insert("executionTime".to_string(), json!(42));
439        
440        let completion = CompletionResult::new(vec![
441            "option1".to_string(),
442            "option2".to_string(),
443            "option3".to_string()
444        ])
445        .with_total(100)
446        .with_has_more(true);
447        
448        let result = CompleteResult::new(completion)
449            .with_meta(meta);
450        
451        let json_value = serde_json::to_value(&result).unwrap();
452        
453        assert!(json_value["completion"].is_object());
454        assert!(json_value["completion"]["values"].is_array());
455        assert_eq!(json_value["completion"]["values"].as_array().unwrap().len(), 3);
456        assert_eq!(json_value["completion"]["values"][0], "option1");
457        assert_eq!(json_value["completion"]["total"], 100);
458        assert_eq!(json_value["completion"]["hasMore"], true);
459        assert_eq!(json_value["_meta"]["executionTime"], 42);
460    }
461
462    #[test]
463    fn test_serialization() {
464        let request = CompleteRequest::new(
465            CompletionReference::resource("file:///test/{id}.txt"),
466            CompleteArgument::new("id", "test")
467        );
468        
469        let json = serde_json::to_string(&request).unwrap();
470        assert!(json.contains("completion/complete"));
471        assert!(json.contains("ref/resource"));
472        assert!(json.contains("file:///test/{id}.txt"));
473        
474        let parsed: CompleteRequest = serde_json::from_str(&json).unwrap();
475        assert_eq!(parsed.method, "completion/complete");
476    }
477}