1use crate::Plugin;
2use serde_json::Value;
3
4#[derive(Debug, Clone)]
6pub struct McpTool {
7 pub name: String,
8 pub description: String,
9 pub input_schema: Value,
10}
11
12#[derive(Debug, Clone)]
14pub struct McpResource {
15 pub uri: String,
16 pub name: String,
17 pub description: String,
18 pub mime_type: String,
19}
20
21#[derive(Debug, Clone)]
23pub struct McpToolResult {
24 pub content: Vec<McpContent>,
25 pub is_error: bool,
26}
27
28#[derive(Debug, Clone)]
29pub struct McpContent {
30 pub content_type: String,
31 pub text: String,
32}
33
34pub struct McpPlugin {
37 app_name: String,
38 entities: Vec<String>,
39 actions: Vec<String>,
40 queries: Vec<String>,
41}
42
43impl McpPlugin {
44 pub fn new(app_name: &str) -> Self {
45 Self {
46 app_name: app_name.to_string(),
47 entities: vec![],
48 actions: vec![],
49 queries: vec![],
50 }
51 }
52
53 pub fn with_entities(mut self, entities: Vec<String>) -> Self {
54 self.entities = entities;
55 self
56 }
57
58 pub fn with_actions(mut self, actions: Vec<String>) -> Self {
59 self.actions = actions;
60 self
61 }
62
63 pub fn with_queries(mut self, queries: Vec<String>) -> Self {
64 self.queries = queries;
65 self
66 }
67
68 pub fn tools(&self) -> Vec<McpTool> {
70 let mut tools = vec![
71 McpTool {
72 name: "list_entities".into(),
73 description: format!("List all rows from an entity in the {} database", self.app_name),
74 input_schema: serde_json::json!({
75 "type": "object",
76 "properties": {
77 "entity": {
78 "type": "string",
79 "description": format!("Entity name. Available: [{}]", self.entities.join(", ")),
80 }
81 },
82 "required": ["entity"]
83 }),
84 },
85 McpTool {
86 name: "get_entity".into(),
87 description: "Get a single row by ID".into(),
88 input_schema: serde_json::json!({
89 "type": "object",
90 "properties": {
91 "entity": { "type": "string", "description": "Entity name" },
92 "id": { "type": "string", "description": "Row ID" }
93 },
94 "required": ["entity", "id"]
95 }),
96 },
97 McpTool {
98 name: "insert_entity".into(),
99 description: "Insert a new row into an entity".into(),
100 input_schema: serde_json::json!({
101 "type": "object",
102 "properties": {
103 "entity": { "type": "string", "description": "Entity name" },
104 "data": { "type": "object", "description": "Row data as key-value pairs" }
105 },
106 "required": ["entity", "data"]
107 }),
108 },
109 McpTool {
110 name: "update_entity".into(),
111 description: "Update an existing row".into(),
112 input_schema: serde_json::json!({
113 "type": "object",
114 "properties": {
115 "entity": { "type": "string", "description": "Entity name" },
116 "id": { "type": "string", "description": "Row ID" },
117 "data": { "type": "object", "description": "Fields to update" }
118 },
119 "required": ["entity", "id", "data"]
120 }),
121 },
122 McpTool {
123 name: "delete_entity".into(),
124 description: "Delete a row by ID".into(),
125 input_schema: serde_json::json!({
126 "type": "object",
127 "properties": {
128 "entity": { "type": "string", "description": "Entity name" },
129 "id": { "type": "string", "description": "Row ID" }
130 },
131 "required": ["entity", "id"]
132 }),
133 },
134 McpTool {
135 name: "search".into(),
136 description: "Search across entities with a text query".into(),
137 input_schema: serde_json::json!({
138 "type": "object",
139 "properties": {
140 "entity": { "type": "string", "description": "Entity to search in" },
141 "query": { "type": "string", "description": "Search text" }
142 },
143 "required": ["entity", "query"]
144 }),
145 },
146 McpTool {
147 name: "inspect_schema".into(),
148 description: format!("Get the full schema of the {} app including entities, fields, queries, actions, and policies", self.app_name),
149 input_schema: serde_json::json!({
150 "type": "object",
151 "properties": {}
152 }),
153 },
154 ];
155
156 for action in &self.actions {
158 tools.push(McpTool {
159 name: format!("action_{action}"),
160 description: format!("Execute the {action} action"),
161 input_schema: serde_json::json!({
162 "type": "object",
163 "properties": {
164 "input": { "type": "object", "description": "Action input data" }
165 },
166 "required": ["input"]
167 }),
168 });
169 }
170
171 tools
172 }
173
174 pub fn resources(&self) -> Vec<McpResource> {
176 let mut resources = vec![McpResource {
177 uri: "pylon://schema".into(),
178 name: "App Schema".into(),
179 description: "The full app manifest/schema".into(),
180 mime_type: "application/json".into(),
181 }];
182
183 for entity in &self.entities {
184 resources.push(McpResource {
185 uri: format!("pylon://entities/{entity}"),
186 name: format!("{entity} data"),
187 description: format!("All rows in the {entity} entity"),
188 mime_type: "application/json".into(),
189 });
190 }
191
192 resources
193 }
194
195 pub fn server_info(&self) -> Value {
197 serde_json::json!({
198 "name": format!("{}-pylon", self.app_name),
199 "version": "0.1.0",
200 "description": format!("MCP server for {} powered by pylon", self.app_name),
201 "tools": self.tools().iter().map(|t| serde_json::json!({
202 "name": t.name,
203 "description": t.description,
204 "inputSchema": t.input_schema,
205 })).collect::<Vec<_>>(),
206 "resources": self.resources().iter().map(|r| serde_json::json!({
207 "uri": r.uri,
208 "name": r.name,
209 "description": r.description,
210 "mimeType": r.mime_type,
211 })).collect::<Vec<_>>(),
212 })
213 }
214}
215
216impl Plugin for McpPlugin {
217 fn name(&self) -> &str {
218 "mcp"
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 fn test_plugin() -> McpPlugin {
227 McpPlugin::new("test-app")
228 .with_entities(vec!["User".into(), "Todo".into()])
229 .with_actions(vec!["createTodo".into(), "toggleTodo".into()])
230 .with_queries(vec!["allTodos".into()])
231 }
232
233 #[test]
234 fn generates_tools() {
235 let plugin = test_plugin();
236 let tools = plugin.tools();
237 assert_eq!(tools.len(), 9);
239 assert!(tools.iter().any(|t| t.name == "list_entities"));
240 assert!(tools.iter().any(|t| t.name == "action_createTodo"));
241 assert!(tools.iter().any(|t| t.name == "inspect_schema"));
242 }
243
244 #[test]
245 fn generates_resources() {
246 let plugin = test_plugin();
247 let resources = plugin.resources();
248 assert_eq!(resources.len(), 3);
250 assert!(resources.iter().any(|r| r.uri == "pylon://schema"));
251 assert!(resources.iter().any(|r| r.uri == "pylon://entities/User"));
252 }
253
254 #[test]
255 fn server_info_is_valid_json() {
256 let plugin = test_plugin();
257 let info = plugin.server_info();
258 assert!(info.get("name").is_some());
259 assert!(info.get("tools").unwrap().as_array().unwrap().len() > 0);
260 assert!(info.get("resources").unwrap().as_array().unwrap().len() > 0);
261 }
262
263 #[test]
264 fn tool_schemas_have_required_fields() {
265 let plugin = test_plugin();
266 let tools = plugin.tools();
267 for tool in &tools {
268 assert!(!tool.name.is_empty());
269 assert!(!tool.description.is_empty());
270 assert!(tool.input_schema.is_object());
271 }
272 }
273
274 #[test]
275 fn entity_names_in_tool_description() {
276 let plugin = test_plugin();
277 let tools = plugin.tools();
278 let list_tool = tools.iter().find(|t| t.name == "list_entities").unwrap();
279 let schema_str = serde_json::to_string(&list_tool.input_schema).unwrap();
280 assert!(schema_str.contains("User"));
281 assert!(schema_str.contains("Todo"));
282 }
283
284 #[test]
285 fn empty_plugin() {
286 let plugin = McpPlugin::new("empty");
287 let tools = plugin.tools();
288 assert_eq!(tools.len(), 7); let resources = plugin.resources();
290 assert_eq!(resources.len(), 1); }
292}