1use std::collections::HashMap;
10
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13
14use crate::bridges::{Bridge, BridgeError, BridgeKind};
15
16#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
17pub struct McpTool {
18 pub name: String,
19 #[serde(skip_serializing_if = "Option::is_none", default)]
20 pub description: Option<String>,
21 #[serde(
22 rename = "inputSchema",
23 skip_serializing_if = "Option::is_none",
24 default
25 )]
26 pub input_schema: Option<Value>,
27}
28
29#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
30pub struct McpToolList {
31 pub tools: Vec<McpTool>,
32}
33
34#[derive(Clone, Debug, Default)]
35pub struct McpImportOptions {
36 pub default_risk: Option<String>,
37 pub default_approval: Option<String>,
38 pub default_proof: Option<String>,
39 pub danger_tag_map: HashMap<String, Vec<String>>,
40 pub name_prefix: Option<String>,
41}
42
43#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
46pub struct McpAction {
47 pub name: String,
48 pub risk: String,
49 pub approval: String,
50 #[serde(skip_serializing_if = "Option::is_none", default)]
51 pub proof: Option<String>,
52 #[serde(skip_serializing_if = "Option::is_none", default)]
53 pub description: Option<String>,
54 #[serde(skip_serializing_if = "Option::is_none", default)]
55 pub parameters: Option<Value>,
56 #[serde(skip_serializing_if = "Option::is_none", default)]
57 pub danger_tags: Option<Vec<String>>,
58}
59
60const ACTION_NAME_RE: &str = r"^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$";
61
62fn normalize_tool_name(name: &str, prefix: Option<&str>) -> String {
63 let mut scrubbed = String::with_capacity(name.len());
64 let mut prev_underscore = false;
65 for ch in name.chars() {
66 if ch.is_ascii_alphanumeric() {
67 scrubbed.push(ch.to_ascii_lowercase());
68 prev_underscore = false;
69 } else if !prev_underscore {
70 scrubbed.push('_');
71 prev_underscore = true;
72 }
73 }
74 let trimmed = scrubbed.trim_matches('_').to_string();
75 let with_prefix = match prefix {
76 Some(p) if !p.is_empty() => format!("{}.{}", p, trimmed),
77 _ => trimmed,
78 };
79 if with_prefix.contains('.') {
80 with_prefix
81 } else {
82 format!("mcp.{}", with_prefix)
83 }
84}
85
86pub fn mcp_to_contract_actions(
87 tool_list: &McpToolList,
88 opts: &McpImportOptions,
89) -> Result<Vec<McpAction>, BridgeError> {
90 let default_risk = opts
91 .default_risk
92 .clone()
93 .unwrap_or_else(|| "R2".to_string());
94 let default_approval = opts
95 .default_approval
96 .clone()
97 .unwrap_or_else(|| "conditional".to_string());
98 let re = regex::Regex::new(ACTION_NAME_RE).expect("static regex");
99 let mut out = Vec::with_capacity(tool_list.tools.len());
100 for tool in &tool_list.tools {
101 if tool.name.is_empty() {
102 return Err(BridgeError::InvalidInput("MCP tool missing a name".into()));
103 }
104 let action_name = normalize_tool_name(&tool.name, opts.name_prefix.as_deref());
105 if !re.is_match(&action_name) {
106 return Err(BridgeError::InvalidInput(format!(
107 "MCP tool {} produced invalid action name {}",
108 tool.name, action_name
109 )));
110 }
111 let danger_tags = opts.danger_tag_map.get(&tool.name).cloned();
112 let action = McpAction {
113 name: action_name,
114 risk: default_risk.clone(),
115 approval: default_approval.clone(),
116 proof: opts.default_proof.clone(),
117 description: tool.description.clone(),
118 parameters: tool.input_schema.clone(),
119 danger_tags: danger_tags.filter(|t| !t.is_empty()),
120 };
121 out.push(action);
122 }
123 Ok(out)
124}
125
126pub fn contract_to_mcp_tools(actions: &[McpAction]) -> McpToolList {
127 let tools = actions
128 .iter()
129 .map(|action| {
130 let warning = match action.danger_tags.as_ref() {
131 Some(tags) if !tags.is_empty() => format!("⚠️ {}. ", tags.join(", ")),
132 _ => String::new(),
133 };
134 let description = format!(
135 "{}{}",
136 warning,
137 action.description.clone().unwrap_or_default()
138 )
139 .trim()
140 .to_string();
141 McpTool {
142 name: action.name.clone(),
143 description: if description.is_empty() {
144 None
145 } else {
146 Some(description)
147 },
148 input_schema: action.parameters.clone(),
149 }
150 })
151 .collect();
152 McpToolList { tools }
153}
154
155#[derive(Clone, Debug, Default)]
156pub struct McpBridgeConfig {
157 pub bridge_id: String,
158 pub trust_domain: String,
159 pub import: McpImportOptions,
160}
161
162pub struct McpBridge {
163 cfg: McpBridgeConfig,
164}
165
166impl McpBridge {
167 pub fn new(cfg: McpBridgeConfig) -> Self {
168 McpBridge { cfg }
169 }
170
171 pub fn import_tools(&self, tool_list: &McpToolList) -> Result<Vec<McpAction>, BridgeError> {
172 mcp_to_contract_actions(tool_list, &self.cfg.import)
173 }
174
175 pub fn export_tools(&self, actions: &[McpAction]) -> McpToolList {
176 contract_to_mcp_tools(actions)
177 }
178
179 pub fn normalize(&self, tool_name: &str) -> String {
181 normalize_tool_name(tool_name, self.cfg.import.name_prefix.as_deref())
182 }
183}
184
185impl Bridge for McpBridge {
186 fn bridge_id(&self) -> &str {
187 &self.cfg.bridge_id
188 }
189 fn kind(&self) -> BridgeKind {
190 BridgeKind::Mcp
191 }
192 fn trust_domain(&self) -> &str {
193 &self.cfg.trust_domain
194 }
195}