cellstate_pipeline/pack/
mcp.rs

1//! MCP (Model Context Protocol) schema generation.
2//!
3//! Generates deterministic `mcp.json` from compiled pack configuration.
4//! The output is canonical JSON with sorted keys for reproducibility.
5
6use crate::compiler::{CompiledConfig, CompiledToolConfig, CompiledToolKind};
7use serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9
10/// MCP schema representation for tool serving.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct McpSchema {
13    /// Schema version
14    pub version: String,
15    /// Tools available via MCP
16    pub tools: Vec<McpTool>,
17    /// File hashes for drift detection
18    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
19    pub file_hashes: BTreeMap<String, String>,
20}
21
22/// Individual tool in MCP schema.
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct McpTool {
25    /// Tool identifier
26    pub name: String,
27    /// Tool description (from prompt or command)
28    pub description: String,
29    /// Input schema (if contract defined)
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub input_schema: Option<serde_json::Value>,
32}
33
34/// Generate deterministic MCP schema from compiled configuration.
35///
36/// The output is canonical:
37/// - Tools are sorted by ID
38/// - File hashes are in a BTreeMap (sorted keys)
39/// - JSON output will be consistent across runs
40pub fn generate_mcp_schema(config: &CompiledConfig) -> McpSchema {
41    let mut tools: Vec<McpTool> = config.tools.iter().map(tool_to_mcp).collect();
42
43    // Sort by name for deterministic output
44    tools.sort_by(|a, b| a.name.cmp(&b.name));
45
46    // Convert file_hashes to BTreeMap for sorted output
47    let file_hashes: BTreeMap<String, String> = config
48        .file_hashes
49        .iter()
50        .map(|(k, v)| (k.clone(), v.clone()))
51        .collect();
52
53    McpSchema {
54        version: "1.0".to_string(),
55        tools,
56        file_hashes,
57    }
58}
59
60fn tool_to_mcp(tool: &CompiledToolConfig) -> McpTool {
61    let description = match tool.kind {
62        CompiledToolKind::Exec => tool.cmd.as_deref().unwrap_or("Executable tool").to_string(),
63        CompiledToolKind::WasmExec => {
64            format!(
65                "WASM sandboxed tool: {}",
66                tool.cmd.as_deref().unwrap_or("unknown module")
67            )
68        }
69        CompiledToolKind::Prompt => {
70            format!(
71                "Prompt tool: {}",
72                tool.prompt_md.as_deref().unwrap_or("unknown")
73            )
74        }
75        CompiledToolKind::Bash => "Bash tool: sandboxed command execution".to_string(),
76        CompiledToolKind::Browser => {
77            "Browser tool: accessibility-first browser automation".to_string()
78        }
79        CompiledToolKind::Composio => {
80            format!(
81                "Composio tool: managed SaaS integration ({})",
82                tool.composio_toolkit.as_deref().unwrap_or("unknown")
83            )
84        }
85        CompiledToolKind::ComposioGateway => {
86            "Search Composio's 800+ tool integrations. Returns available actions for a given use case.".to_string()
87        }
88    };
89
90    McpTool {
91        name: tool.id.clone(),
92        description,
93        input_schema: tool.compiled_schema.clone(),
94    }
95}
96
97/// Serialize MCP schema to canonical JSON.
98///
99/// Uses sorted keys and consistent formatting for byte-identical output.
100pub fn to_canonical_json(schema: &McpSchema) -> Result<String, serde_json::Error> {
101    serde_json::to_string_pretty(schema)
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn test_deterministic_output() {
110        let config1 = CompiledConfig::default();
111        let config2 = CompiledConfig::default();
112
113        let json1 =
114            to_canonical_json(&generate_mcp_schema(&config1)).expect("valid JSON serialization");
115        let json2 =
116            to_canonical_json(&generate_mcp_schema(&config2)).expect("valid JSON serialization");
117
118        assert_eq!(json1, json2, "Same config should produce identical JSON");
119    }
120}