1use crate::compiler::models::*;
6
7pub fn generate_mcp(schema: &CompiledSchema) -> String {
9 let domain = &schema.domain;
10 let domain_prefix = domain.replace(['.', '-'], "_");
11
12 let mut tools: Vec<serde_json::Value> = Vec::new();
13
14 for model in &schema.models {
16 if model.instance_count <= 1 {
17 continue;
18 }
19
20 let model_lower = model.name.to_lowercase();
21
22 let mut search_props = serde_json::Map::new();
24 search_props.insert(
25 "query".to_string(),
26 serde_json::json!({"type": "string", "description": format!("Search query for {}s", model.name)}),
27 );
28 search_props.insert(
29 "limit".to_string(),
30 serde_json::json!({"type": "integer", "description": "Max results", "default": 20}),
31 );
32
33 for field in &model.fields {
34 if let Some(_dim) = field.feature_dim {
35 match field.field_type {
36 FieldType::Float | FieldType::Integer => {
37 search_props.insert(
38 format!("{}_min", field.name),
39 serde_json::json!({
40 "type": "number",
41 "description": format!("Minimum {}", field.name)
42 }),
43 );
44 search_props.insert(
45 format!("{}_max", field.name),
46 serde_json::json!({
47 "type": "number",
48 "description": format!("Maximum {}", field.name)
49 }),
50 );
51 }
52 _ => {}
53 }
54 }
55 }
56
57 tools.push(serde_json::json!({
58 "name": format!("{domain_prefix}_search_{model_lower}s"),
59 "description": format!("Search {}s on {domain}", model.name),
60 "inputSchema": {
61 "type": "object",
62 "properties": search_props,
63 "required": ["query"]
64 }
65 }));
66
67 tools.push(serde_json::json!({
69 "name": format!("{domain_prefix}_get_{model_lower}"),
70 "description": format!("Get a specific {} by node ID from {domain}", model.name),
71 "inputSchema": {
72 "type": "object",
73 "properties": {
74 "node_id": {
75 "type": "integer",
76 "description": format!("Node ID of the {}", model.name)
77 }
78 },
79 "required": ["node_id"]
80 }
81 }));
82 }
83
84 for action in &schema.actions {
86 let mut props = serde_json::Map::new();
87
88 if action.is_instance_method {
89 props.insert(
90 "node_id".to_string(),
91 serde_json::json!({
92 "type": "integer",
93 "description": format!("Node ID of the {}", action.belongs_to)
94 }),
95 );
96 }
97
98 let mut required: Vec<String> = Vec::new();
99 if action.is_instance_method {
100 required.push("node_id".to_string());
101 }
102
103 for param in &action.params {
104 if param.name == "node_id" {
105 continue;
106 }
107 let type_str = match param.param_type {
108 FieldType::Integer => "integer",
109 FieldType::Float => "number",
110 FieldType::Bool => "boolean",
111 _ => "string",
112 };
113 let mut prop = serde_json::json!({
114 "type": type_str,
115 "description": format!("{} parameter", param.name)
116 });
117 if let Some(ref default) = param.default_value {
118 prop["default"] = serde_json::Value::String(default.clone());
119 }
120 props.insert(param.name.clone(), prop);
121 if param.required {
122 required.push(param.name.clone());
123 }
124 }
125
126 tools.push(serde_json::json!({
127 "name": format!("{domain_prefix}_{}", action.name),
128 "description": format!("{} on {domain}", action.name.replace('_', " ")),
129 "inputSchema": {
130 "type": "object",
131 "properties": props,
132 "required": required
133 }
134 }));
135 }
136
137 let output = serde_json::json!({ "tools": tools });
138 serde_json::to_string_pretty(&output).unwrap_or_default()
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use chrono::Utc;
145
146 fn test_schema() -> CompiledSchema {
147 CompiledSchema {
148 domain: "shop.com".to_string(),
149 compiled_at: Utc::now(),
150 models: vec![DataModel {
151 name: "Product".to_string(),
152 schema_org_type: "Product".to_string(),
153 fields: vec![ModelField {
154 name: "price".to_string(),
155 field_type: FieldType::Float,
156 source: FieldSource::JsonLd,
157 confidence: 0.99,
158 nullable: true,
159 example_values: vec![],
160 feature_dim: Some(48),
161 }],
162 instance_count: 50,
163 example_urls: vec![],
164 search_action: None,
165 list_url: None,
166 }],
167 actions: vec![CompiledAction {
168 name: "add_to_cart".to_string(),
169 belongs_to: "Product".to_string(),
170 is_instance_method: true,
171 http_method: "POST".to_string(),
172 endpoint_template: "/cart/add".to_string(),
173 params: vec![ActionParam {
174 name: "quantity".to_string(),
175 param_type: FieldType::Integer,
176 required: false,
177 default_value: Some("1".to_string()),
178 source: "json_body".to_string(),
179 }],
180 requires_auth: false,
181 execution_path: "http".to_string(),
182 confidence: 0.9,
183 }],
184 relationships: vec![],
185 stats: SchemaStats {
186 total_models: 1,
187 total_fields: 1,
188 total_instances: 50,
189 avg_confidence: 0.99,
190 },
191 }
192 }
193
194 #[test]
195 fn test_generate_mcp_tools() {
196 let json = generate_mcp(&test_schema());
197 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
198
199 let tools = parsed["tools"].as_array().unwrap();
200 assert!(tools.len() >= 2, "should have search + action tools");
201
202 let search = tools
204 .iter()
205 .find(|t| t["name"].as_str().unwrap().contains("search"));
206 assert!(search.is_some(), "should have search tool");
207
208 let cart = tools
210 .iter()
211 .find(|t| t["name"].as_str().unwrap().contains("add_to_cart"));
212 assert!(cart.is_some(), "should have add_to_cart tool");
213 }
214
215 #[test]
216 fn test_mcp_valid_json() {
217 let json = generate_mcp(&test_schema());
218 let result: Result<serde_json::Value, _> = serde_json::from_str(&json);
219 assert!(result.is_ok(), "output should be valid JSON");
220 }
221}