cellstate_pipeline/pack/
mcp.rs1use crate::compiler::{CompiledConfig, CompiledToolConfig, CompiledToolKind};
7use serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct McpSchema {
13 pub version: String,
15 pub tools: Vec<McpTool>,
17 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
19 pub file_hashes: BTreeMap<String, String>,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct McpTool {
25 pub name: String,
27 pub description: String,
29 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub input_schema: Option<serde_json::Value>,
32}
33
34pub fn generate_mcp_schema(config: &CompiledConfig) -> McpSchema {
41 let mut tools: Vec<McpTool> = config.tools.iter().map(tool_to_mcp).collect();
42
43 tools.sort_by(|a, b| a.name.cmp(&b.name));
45
46 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
97pub 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}