Skip to main content

dcp/cli/
convert.rs

1//! MCP to DCP schema conversion utilities.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6/// MCP JSON schema representation
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct McpSchema {
9    /// Tool name
10    pub name: String,
11    /// Tool description
12    #[serde(default)]
13    pub description: String,
14    /// Input schema
15    #[serde(rename = "inputSchema", default)]
16    pub input_schema: McpInputSchema,
17}
18
19/// MCP input schema
20#[derive(Debug, Clone, Default, Serialize, Deserialize)]
21pub struct McpInputSchema {
22    /// Schema type (usually "object")
23    #[serde(rename = "type", default)]
24    pub schema_type: String,
25    /// Properties
26    #[serde(default)]
27    pub properties: HashMap<String, McpProperty>,
28    /// Required fields
29    #[serde(default)]
30    pub required: Vec<String>,
31}
32
33/// MCP property definition
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct McpProperty {
36    /// Property type
37    #[serde(rename = "type")]
38    pub prop_type: String,
39    /// Description
40    #[serde(default)]
41    pub description: String,
42    /// Enum values (if applicable)
43    #[serde(rename = "enum", default)]
44    pub enum_values: Vec<String>,
45    /// Default value
46    #[serde(default)]
47    pub default: Option<serde_json::Value>,
48}
49
50/// DCP binary schema representation
51#[derive(Debug, Clone)]
52pub struct DcpSchema {
53    /// Tool name
54    pub name: String,
55    /// Tool ID (assigned during conversion)
56    pub tool_id: u16,
57    /// Description
58    pub description: String,
59    /// Field definitions
60    pub fields: Vec<DcpField>,
61    /// Required fields bitmask
62    pub required_mask: u64,
63}
64
65/// DCP field definition
66#[derive(Debug, Clone)]
67pub struct DcpField {
68    /// Field name
69    pub name: String,
70    /// Field type
71    pub field_type: DcpFieldType,
72    /// Offset in binary layout
73    pub offset: u16,
74    /// Size in bytes
75    pub size: u16,
76}
77
78/// DCP field types
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80#[repr(u8)]
81pub enum DcpFieldType {
82    Null = 0,
83    Bool = 1,
84    I32 = 2,
85    I64 = 3,
86    F64 = 4,
87    String = 5,
88    Bytes = 6,
89    Array = 7,
90    Object = 8,
91}
92
93impl DcpFieldType {
94    /// Get the default size for this type
95    pub fn default_size(&self) -> u16 {
96        match self {
97            Self::Null => 0,
98            Self::Bool => 1,
99            Self::I32 => 4,
100            Self::I64 => 8,
101            Self::F64 => 8,
102            Self::String => 8, // offset + length
103            Self::Bytes => 8,  // offset + length
104            Self::Array => 8,  // offset + length
105            Self::Object => 8, // offset + length
106        }
107    }
108
109    /// Convert from MCP type string
110    pub fn from_mcp_type(mcp_type: &str) -> Self {
111        match mcp_type {
112            "boolean" => Self::Bool,
113            "integer" => Self::I64,
114            "number" => Self::F64,
115            "string" => Self::String,
116            "array" => Self::Array,
117            "object" => Self::Object,
118            "null" => Self::Null,
119            _ => Self::String, // Default to string for unknown types
120        }
121    }
122}
123
124impl DcpSchema {
125    /// Serialize schema to binary format
126    pub fn to_bytes(&self) -> Vec<u8> {
127        let mut bytes = Vec::new();
128
129        // Header: magic (2) + version (1) + tool_id (2) + name_len (1) + desc_len (2) + field_count (1) + required_mask (8)
130        bytes.extend_from_slice(&[0xDC, 0x53]); // Magic "DCS" for DCP Schema
131        bytes.push(1); // Version 1
132        bytes.extend_from_slice(&self.tool_id.to_le_bytes());
133
134        let name_bytes = self.name.as_bytes();
135        bytes.push(name_bytes.len() as u8);
136
137        let desc_bytes = self.description.as_bytes();
138        bytes.extend_from_slice(&(desc_bytes.len() as u16).to_le_bytes());
139
140        bytes.push(self.fields.len() as u8);
141        bytes.extend_from_slice(&self.required_mask.to_le_bytes());
142
143        // Name
144        bytes.extend_from_slice(name_bytes);
145
146        // Description
147        bytes.extend_from_slice(desc_bytes);
148
149        // Fields
150        for field in &self.fields {
151            let field_name = field.name.as_bytes();
152            bytes.push(field_name.len() as u8);
153            bytes.extend_from_slice(field_name);
154            bytes.push(field.field_type as u8);
155            bytes.extend_from_slice(&field.offset.to_le_bytes());
156            bytes.extend_from_slice(&field.size.to_le_bytes());
157        }
158
159        bytes
160    }
161
162    /// Deserialize schema from binary format
163    pub fn from_bytes(bytes: &[u8]) -> Result<Self, String> {
164        if bytes.len() < 17 {
165            return Err("Insufficient data for schema header".to_string());
166        }
167
168        // Check magic
169        if bytes[0] != 0xDC || bytes[1] != 0x53 {
170            return Err("Invalid schema magic number".to_string());
171        }
172
173        // Check version
174        if bytes[2] != 1 {
175            return Err(format!("Unsupported schema version: {}", bytes[2]));
176        }
177
178        let tool_id = u16::from_le_bytes([bytes[3], bytes[4]]);
179        let name_len = bytes[5] as usize;
180        let desc_len = u16::from_le_bytes([bytes[6], bytes[7]]) as usize;
181        let field_count = bytes[8] as usize;
182        let required_mask = u64::from_le_bytes([
183            bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15], bytes[16],
184        ]);
185
186        let mut offset = 17;
187
188        // Read name
189        if offset + name_len > bytes.len() {
190            return Err("Insufficient data for name".to_string());
191        }
192        let name = String::from_utf8(bytes[offset..offset + name_len].to_vec())
193            .map_err(|e| format!("Invalid name encoding: {}", e))?;
194        offset += name_len;
195
196        // Read description
197        if offset + desc_len > bytes.len() {
198            return Err("Insufficient data for description".to_string());
199        }
200        let description = String::from_utf8(bytes[offset..offset + desc_len].to_vec())
201            .map_err(|e| format!("Invalid description encoding: {}", e))?;
202        offset += desc_len;
203
204        // Read fields
205        let mut fields = Vec::with_capacity(field_count);
206        for _ in 0..field_count {
207            if offset >= bytes.len() {
208                return Err("Insufficient data for field".to_string());
209            }
210
211            let field_name_len = bytes[offset] as usize;
212            offset += 1;
213
214            if offset + field_name_len + 5 > bytes.len() {
215                return Err("Insufficient data for field data".to_string());
216            }
217
218            let field_name = String::from_utf8(bytes[offset..offset + field_name_len].to_vec())
219                .map_err(|e| format!("Invalid field name encoding: {}", e))?;
220            offset += field_name_len;
221
222            let field_type = match bytes[offset] {
223                0 => DcpFieldType::Null,
224                1 => DcpFieldType::Bool,
225                2 => DcpFieldType::I32,
226                3 => DcpFieldType::I64,
227                4 => DcpFieldType::F64,
228                5 => DcpFieldType::String,
229                6 => DcpFieldType::Bytes,
230                7 => DcpFieldType::Array,
231                8 => DcpFieldType::Object,
232                _ => return Err(format!("Invalid field type: {}", bytes[offset])),
233            };
234            offset += 1;
235
236            let field_offset = u16::from_le_bytes([bytes[offset], bytes[offset + 1]]);
237            offset += 2;
238
239            let field_size = u16::from_le_bytes([bytes[offset], bytes[offset + 1]]);
240            offset += 2;
241
242            fields.push(DcpField {
243                name: field_name,
244                field_type,
245                offset: field_offset,
246                size: field_size,
247            });
248        }
249
250        Ok(Self {
251            name,
252            tool_id,
253            description,
254            fields,
255            required_mask,
256        })
257    }
258}
259
260/// Convert MCP schema to DCP schema
261pub fn convert_mcp_to_dcp(mcp: &McpSchema) -> Result<DcpSchema, String> {
262    // Generate tool ID from name hash
263    let tool_id = {
264        let hash = blake3::hash(mcp.name.as_bytes());
265        let bytes = hash.as_bytes();
266        u16::from_le_bytes([bytes[0], bytes[1]])
267    };
268
269    // Convert fields
270    let mut fields = Vec::new();
271    let mut current_offset: u16 = 0;
272    let mut required_mask: u64 = 0;
273
274    // Sort properties by name for consistent ordering
275    let mut prop_names: Vec<_> = mcp.input_schema.properties.keys().collect();
276    prop_names.sort();
277
278    for (idx, name) in prop_names.iter().enumerate() {
279        if idx >= 64 {
280            return Err("Too many fields (max 64)".to_string());
281        }
282
283        let prop = &mcp.input_schema.properties[*name];
284        let field_type = DcpFieldType::from_mcp_type(&prop.prop_type);
285        let size = field_type.default_size();
286
287        fields.push(DcpField {
288            name: (*name).clone(),
289            field_type,
290            offset: current_offset,
291            size,
292        });
293
294        // Check if required
295        if mcp.input_schema.required.contains(name) {
296            required_mask |= 1 << idx;
297        }
298
299        current_offset += size;
300    }
301
302    Ok(DcpSchema {
303        name: mcp.name.clone(),
304        tool_id,
305        description: mcp.description.clone(),
306        fields,
307        required_mask,
308    })
309}
310
311/// Convert DCP schema back to MCP schema
312pub fn convert_dcp_to_mcp(dcp: &DcpSchema) -> McpSchema {
313    let mut properties = HashMap::new();
314
315    for field in dcp.fields.iter() {
316        let prop_type = match field.field_type {
317            DcpFieldType::Null => "null",
318            DcpFieldType::Bool => "boolean",
319            DcpFieldType::I32 | DcpFieldType::I64 => "integer",
320            DcpFieldType::F64 => "number",
321            DcpFieldType::String => "string",
322            DcpFieldType::Bytes => "string", // Base64 encoded
323            DcpFieldType::Array => "array",
324            DcpFieldType::Object => "object",
325        };
326
327        properties.insert(
328            field.name.clone(),
329            McpProperty {
330                prop_type: prop_type.to_string(),
331                description: String::new(),
332                enum_values: Vec::new(),
333                default: None,
334            },
335        );
336    }
337
338    // Build required list
339    let required: Vec<String> = dcp
340        .fields
341        .iter()
342        .enumerate()
343        .filter(|(idx, _)| (dcp.required_mask & (1 << idx)) != 0)
344        .map(|(_, field)| field.name.clone())
345        .collect();
346
347    McpSchema {
348        name: dcp.name.clone(),
349        description: dcp.description.clone(),
350        input_schema: McpInputSchema {
351            schema_type: "object".to_string(),
352            properties,
353            required,
354        },
355    }
356}
357
358#[cfg(test)]
359mod tests {
360    use super::*;
361
362    #[test]
363    fn test_convert_simple_schema() {
364        let mcp = McpSchema {
365            name: "test_tool".to_string(),
366            description: "A test tool".to_string(),
367            input_schema: McpInputSchema {
368                schema_type: "object".to_string(),
369                properties: {
370                    let mut props = HashMap::new();
371                    props.insert(
372                        "name".to_string(),
373                        McpProperty {
374                            prop_type: "string".to_string(),
375                            description: "The name".to_string(),
376                            enum_values: Vec::new(),
377                            default: None,
378                        },
379                    );
380                    props.insert(
381                        "count".to_string(),
382                        McpProperty {
383                            prop_type: "integer".to_string(),
384                            description: "The count".to_string(),
385                            enum_values: Vec::new(),
386                            default: None,
387                        },
388                    );
389                    props
390                },
391                required: vec!["name".to_string()],
392            },
393        };
394
395        let dcp = convert_mcp_to_dcp(&mcp).unwrap();
396
397        assert_eq!(dcp.name, "test_tool");
398        assert_eq!(dcp.description, "A test tool");
399        assert_eq!(dcp.fields.len(), 2);
400    }
401
402    #[test]
403    fn test_schema_round_trip() {
404        let mcp = McpSchema {
405            name: "round_trip".to_string(),
406            description: "Test round trip".to_string(),
407            input_schema: McpInputSchema {
408                schema_type: "object".to_string(),
409                properties: {
410                    let mut props = HashMap::new();
411                    props.insert(
412                        "field1".to_string(),
413                        McpProperty {
414                            prop_type: "string".to_string(),
415                            description: String::new(),
416                            enum_values: Vec::new(),
417                            default: None,
418                        },
419                    );
420                    props
421                },
422                required: vec!["field1".to_string()],
423            },
424        };
425
426        let dcp = convert_mcp_to_dcp(&mcp).unwrap();
427        let bytes = dcp.to_bytes();
428        let restored = DcpSchema::from_bytes(&bytes).unwrap();
429
430        assert_eq!(restored.name, dcp.name);
431        assert_eq!(restored.tool_id, dcp.tool_id);
432        assert_eq!(restored.fields.len(), dcp.fields.len());
433        assert_eq!(restored.required_mask, dcp.required_mask);
434    }
435
436    #[test]
437    fn test_field_type_conversion() {
438        assert_eq!(DcpFieldType::from_mcp_type("boolean"), DcpFieldType::Bool);
439        assert_eq!(DcpFieldType::from_mcp_type("integer"), DcpFieldType::I64);
440        assert_eq!(DcpFieldType::from_mcp_type("number"), DcpFieldType::F64);
441        assert_eq!(DcpFieldType::from_mcp_type("string"), DcpFieldType::String);
442        assert_eq!(DcpFieldType::from_mcp_type("array"), DcpFieldType::Array);
443        assert_eq!(DcpFieldType::from_mcp_type("object"), DcpFieldType::Object);
444    }
445
446    #[test]
447    fn test_dcp_to_mcp_conversion() {
448        let dcp = DcpSchema {
449            name: "test".to_string(),
450            tool_id: 1,
451            description: "Test".to_string(),
452            fields: vec![DcpField {
453                name: "field1".to_string(),
454                field_type: DcpFieldType::String,
455                offset: 0,
456                size: 8,
457            }],
458            required_mask: 1,
459        };
460
461        let mcp = convert_dcp_to_mcp(&dcp);
462
463        assert_eq!(mcp.name, "test");
464        assert!(mcp.input_schema.properties.contains_key("field1"));
465        assert!(mcp.input_schema.required.contains(&"field1".to_string()));
466    }
467}