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/// Completion result (per MCP spec)
128#[derive(Debug, Clone, Serialize, Deserialize)]
129#[serde(rename_all = "camelCase")]
130pub struct CompletionResult {
131    /// The completion values
132    pub values: Vec<String>,
133    /// Optional total number of possible completions
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub total: Option<u32>,
136    /// Whether there are more completions available
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub has_more: Option<bool>,
139}
140
141/// Complete completion/complete response (per MCP spec)
142#[derive(Debug, Clone, Serialize, Deserialize)]
143#[serde(rename_all = "camelCase")]
144pub struct CompleteResult {
145    /// The completion result
146    pub completion: CompletionResult,
147    /// Meta information (follows MCP Result interface)
148    #[serde(
149        default,
150        skip_serializing_if = "Option::is_none",
151        alias = "_meta",
152        rename = "_meta"
153    )]
154    pub meta: Option<HashMap<String, Value>>,
155}
156
157impl CompletionResult {
158    pub fn new(values: Vec<String>) -> Self {
159        Self {
160            values,
161            total: None,
162            has_more: None,
163        }
164    }
165
166    pub fn with_total(mut self, total: u32) -> Self {
167        self.total = Some(total);
168        self
169    }
170
171    pub fn with_has_more(mut self, has_more: bool) -> Self {
172        self.has_more = Some(has_more);
173        self
174    }
175}
176
177impl CompleteResult {
178    pub fn new(completion: CompletionResult) -> Self {
179        Self {
180            completion,
181            meta: None,
182        }
183    }
184
185    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
186        self.meta = Some(meta);
187        self
188    }
189}
190
191/// Convenience constructors
192impl ResourceTemplateReference {
193    pub fn new(uri: impl Into<String>) -> Self {
194        Self {
195            ref_type: "ref/resource".to_string(),
196            uri: uri.into(),
197        }
198    }
199}
200
201impl PromptReference {
202    pub fn new(name: impl Into<String>) -> Self {
203        Self {
204            ref_type: "ref/prompt".to_string(),
205            name: name.into(),
206            description: None,
207        }
208    }
209
210    pub fn with_description(mut self, description: impl Into<String>) -> Self {
211        self.description = Some(description.into());
212        self
213    }
214}
215
216impl CompletionReference {
217    pub fn resource(uri: impl Into<String>) -> Self {
218        Self::ResourceTemplate(ResourceTemplateReference::new(uri))
219    }
220
221    pub fn prompt(name: impl Into<String>) -> Self {
222        Self::Prompt(PromptReference::new(name))
223    }
224}
225
226impl CompleteArgument {
227    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
228        Self {
229            name: name.into(),
230            value: value.into(),
231        }
232    }
233}
234
235impl Default for CompletionContext {
236    fn default() -> Self {
237        Self::new()
238    }
239}
240
241impl CompletionContext {
242    pub fn new() -> Self {
243        Self { arguments: None }
244    }
245
246    pub fn with_arguments(mut self, arguments: HashMap<String, String>) -> Self {
247        self.arguments = Some(arguments);
248        self
249    }
250}
251
252// Trait implementations for protocol compliance
253use crate::traits::*;
254
255impl Params for CompleteParams {}
256
257impl HasMetaParam for CompleteParams {
258    fn meta(&self) -> Option<&HashMap<String, Value>> {
259        self.meta.as_ref()
260    }
261}
262
263impl HasMethod for CompleteRequest {
264    fn method(&self) -> &str {
265        &self.method
266    }
267}
268
269impl HasParams for CompleteRequest {
270    fn params(&self) -> Option<&dyn Params> {
271        Some(&self.params)
272    }
273}
274
275impl HasData for CompleteResult {
276    fn data(&self) -> HashMap<String, Value> {
277        let mut data = HashMap::new();
278        data.insert(
279            "completion".to_string(),
280            serde_json::to_value(&self.completion).unwrap_or(Value::Null),
281        );
282        data
283    }
284}
285
286impl HasMeta for CompleteResult {
287    fn meta(&self) -> Option<HashMap<String, Value>> {
288        self.meta.clone()
289    }
290}
291
292impl RpcResult for CompleteResult {}
293
294// ===========================================
295// === Fine-Grained Completion Traits ===
296// ===========================================
297
298/// Trait for completion metadata (method, reference type)
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303    use serde_json::json;
304
305    #[test]
306    fn test_resource_template_reference() {
307        let ref_obj = ResourceTemplateReference::new("file:///test/{name}.txt");
308
309        assert_eq!(ref_obj.ref_type, "ref/resource");
310        assert_eq!(ref_obj.uri, "file:///test/{name}.txt");
311
312        let json_value = serde_json::to_value(&ref_obj).unwrap();
313        assert_eq!(json_value["type"], "ref/resource");
314        assert_eq!(json_value["uri"], "file:///test/{name}.txt");
315    }
316
317    #[test]
318    fn test_prompt_reference() {
319        let ref_obj = PromptReference::new("test_prompt").with_description("A test prompt");
320
321        assert_eq!(ref_obj.ref_type, "ref/prompt");
322        assert_eq!(ref_obj.name, "test_prompt");
323        assert_eq!(ref_obj.description, Some("A test prompt".to_string()));
324
325        let json_value = serde_json::to_value(&ref_obj).unwrap();
326        assert_eq!(json_value["type"], "ref/prompt");
327        assert_eq!(json_value["name"], "test_prompt");
328        assert_eq!(json_value["description"], "A test prompt");
329    }
330
331    #[test]
332    fn test_completion_reference_union() {
333        let resource_ref = CompletionReference::resource("file:///test.txt");
334        let prompt_ref = CompletionReference::prompt("my_prompt");
335
336        // Test serialization
337        let resource_json = serde_json::to_value(&resource_ref).unwrap();
338        let prompt_json = serde_json::to_value(&prompt_ref).unwrap();
339
340        assert_eq!(resource_json["type"], "ref/resource");
341        assert_eq!(resource_json["uri"], "file:///test.txt");
342
343        assert_eq!(prompt_json["type"], "ref/prompt");
344        assert_eq!(prompt_json["name"], "my_prompt");
345    }
346
347    #[test]
348    fn test_complete_request_matches_typescript_spec() {
349        // Test CompleteRequest matches: { method: string, params: { ref: ..., argument: ..., context?: ..., _meta?: ... } }
350        let mut meta = HashMap::new();
351        meta.insert("requestId".to_string(), json!("req-123"));
352
353        let mut context_args = HashMap::new();
354        context_args.insert("userId".to_string(), "123".to_string());
355
356        let context = CompletionContext::new().with_arguments(context_args);
357
358        let request = CompleteRequest::new(
359            CompletionReference::prompt("test_prompt"),
360            CompleteArgument::new("arg_name", "partial_value"),
361        )
362        .with_context(context)
363        .with_meta(meta);
364
365        let json_value = serde_json::to_value(&request).unwrap();
366
367        assert_eq!(json_value["method"], "completion/complete");
368        assert!(json_value["params"].is_object());
369        assert!(json_value["params"]["ref"].is_object());
370        assert_eq!(json_value["params"]["ref"]["type"], "ref/prompt");
371        assert_eq!(json_value["params"]["ref"]["name"], "test_prompt");
372        assert_eq!(json_value["params"]["argument"]["name"], "arg_name");
373        assert_eq!(json_value["params"]["argument"]["value"], "partial_value");
374        assert!(json_value["params"]["context"].is_object());
375        assert_eq!(
376            json_value["params"]["context"]["arguments"]["userId"],
377            "123"
378        );
379        assert_eq!(json_value["params"]["_meta"]["requestId"], "req-123");
380    }
381
382    #[test]
383    fn test_complete_result_matches_typescript_spec() {
384        // Test CompleteResult matches: { completion: { values: string[], total?: number, hasMore?: boolean }, _meta?: ... }
385        let mut meta = HashMap::new();
386        meta.insert("executionTime".to_string(), json!(42));
387
388        let completion = CompletionResult::new(vec![
389            "option1".to_string(),
390            "option2".to_string(),
391            "option3".to_string(),
392        ])
393        .with_total(100)
394        .with_has_more(true);
395
396        let result = CompleteResult::new(completion).with_meta(meta);
397
398        let json_value = serde_json::to_value(&result).unwrap();
399
400        assert!(json_value["completion"].is_object());
401        assert!(json_value["completion"]["values"].is_array());
402        assert_eq!(
403            json_value["completion"]["values"].as_array().unwrap().len(),
404            3
405        );
406        assert_eq!(json_value["completion"]["values"][0], "option1");
407        assert_eq!(json_value["completion"]["total"], 100);
408        assert_eq!(json_value["completion"]["hasMore"], true);
409        assert_eq!(json_value["_meta"]["executionTime"], 42);
410    }
411
412    #[test]
413    fn test_serialization() {
414        let request = CompleteRequest::new(
415            CompletionReference::resource("file:///test/{id}.txt"),
416            CompleteArgument::new("id", "test"),
417        );
418
419        let json = serde_json::to_string(&request).unwrap();
420        assert!(json.contains("completion/complete"));
421        assert!(json.contains("ref/resource"));
422        assert!(json.contains("file:///test/{id}.txt"));
423
424        let parsed: CompleteRequest = serde_json::from_str(&json).unwrap();
425        assert_eq!(parsed.method, "completion/complete");
426    }
427}