1use std::collections::HashMap;
37
38use serde::{Deserialize, Serialize};
39use serde_json::Value;
40
41#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
47pub struct ToolDefinition {
48 pub name: String,
50 pub description: String,
52 pub input_schema: Value,
54 pub required_permission: Permission,
56}
57
58#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
60pub struct ToolCallRequest {
61 pub name: String,
63 pub arguments: Value,
65}
66
67#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
69pub struct ToolCallResponse {
70 pub success: bool,
72 pub result: Value,
74 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub error_code: Option<String>,
77}
78
79#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
81#[serde(rename_all = "snake_case")]
82pub enum Permission {
83 Read,
85 Write,
87 Admin,
89}
90
91#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
93pub struct AgentIdentity {
94 pub name: String,
96 pub permissions: Vec<Permission>,
98}
99
100impl AgentIdentity {
101 pub fn has_permission(&self, perm: Permission) -> bool {
103 self.permissions.contains(&perm)
104 }
105
106 pub fn read_only(name: &str) -> Self {
108 Self {
109 name: name.to_string(),
110 permissions: vec![Permission::Read],
111 }
112 }
113
114 pub fn read_write(name: &str) -> Self {
116 Self {
117 name: name.to_string(),
118 permissions: vec![Permission::Read, Permission::Write],
119 }
120 }
121
122 pub fn admin(name: &str) -> Self {
124 Self {
125 name: name.to_string(),
126 permissions: vec![Permission::Read, Permission::Write, Permission::Admin],
127 }
128 }
129}
130
131pub struct McpServer {
138 tools: HashMap<String, ToolDefinition>,
139}
140
141impl McpServer {
142 pub fn new() -> Self {
144 let mut tools = HashMap::new();
145
146 for tool in standard_tools() {
147 tools.insert(tool.name.clone(), tool);
148 }
149
150 Self { tools }
151 }
152
153 pub fn list_tools(&self) -> Vec<&ToolDefinition> {
155 self.tools.values().collect()
156 }
157
158 pub fn list_tools_for(&self, agent: &AgentIdentity) -> Vec<&ToolDefinition> {
160 self.tools
161 .values()
162 .filter(|t| agent.has_permission(t.required_permission))
163 .collect()
164 }
165
166 pub fn get_tool(&self, name: &str) -> Option<&ToolDefinition> {
168 self.tools.get(name)
169 }
170
171 pub fn execute(
178 &self,
179 request: &ToolCallRequest,
180 agent: &AgentIdentity,
181 handler: &dyn ToolHandler,
182 ) -> ToolCallResponse {
183 let Some(tool) = self.tools.get(&request.name) else {
185 return ToolCallResponse {
186 success: false,
187 result: Value::String(format!("unknown tool: {}", request.name)),
188 error_code: Some("unknown_tool".to_string()),
189 };
190 };
191
192 if !agent.has_permission(tool.required_permission) {
194 return ToolCallResponse {
195 success: false,
196 result: Value::String(format!(
197 "permission denied: tool '{}' requires {:?}, agent '{}' has {:?}",
198 tool.name, tool.required_permission, agent.name, agent.permissions
199 )),
200 error_code: Some("permission_denied".to_string()),
201 };
202 }
203
204 if let Err(err) = validate_arguments(&tool.input_schema, &request.arguments) {
206 return ToolCallResponse {
207 success: false,
208 result: Value::String(format!("invalid arguments: {err}")),
209 error_code: Some("invalid_arguments".to_string()),
210 };
211 }
212
213 handler.execute_tool(&request.name, &request.arguments)
215 }
216
217 pub fn tool_count(&self) -> usize {
219 self.tools.len()
220 }
221}
222
223impl Default for McpServer {
224 fn default() -> Self {
225 Self::new()
226 }
227}
228
229pub trait ToolHandler {
232 fn execute_tool(&self, name: &str, arguments: &Value) -> ToolCallResponse;
233}
234
235pub struct MockToolHandler {
237 responses: HashMap<String, Value>,
238}
239
240impl MockToolHandler {
241 pub fn new() -> Self {
242 Self {
243 responses: HashMap::new(),
244 }
245 }
246
247 pub fn with_response(mut self, tool_name: &str, response: Value) -> Self {
248 self.responses.insert(tool_name.to_string(), response);
249 self
250 }
251}
252
253impl Default for MockToolHandler {
254 fn default() -> Self {
255 Self::new()
256 }
257}
258
259impl ToolHandler for MockToolHandler {
260 fn execute_tool(&self, name: &str, _arguments: &Value) -> ToolCallResponse {
261 if let Some(result) = self.responses.get(name) {
262 ToolCallResponse {
263 success: true,
264 result: result.clone(),
265 error_code: None,
266 }
267 } else {
268 ToolCallResponse {
269 success: true,
270 result: Value::String(format!("mock response for {name}")),
271 error_code: None,
272 }
273 }
274 }
275}
276
277fn standard_tools() -> Vec<ToolDefinition> {
282 vec![
283 ToolDefinition {
284 name: "list_routes".to_string(),
285 description: "List all routes in the current gateway config".to_string(),
286 input_schema: serde_json::json!({
287 "type": "object",
288 "properties": {},
289 "additionalProperties": false
290 }),
291 required_permission: Permission::Read,
292 },
293 ToolDefinition {
294 name: "get_route".to_string(),
295 description: "Get a single route by name".to_string(),
296 input_schema: serde_json::json!({
297 "type": "object",
298 "properties": {
299 "name": {"type": "string", "description": "The route name"}
300 },
301 "required": ["name"],
302 "additionalProperties": false
303 }),
304 required_permission: Permission::Read,
305 },
306 ToolDefinition {
307 name: "create_route".to_string(),
308 description: "Create a new route".to_string(),
309 input_schema: serde_json::json!({
310 "type": "object",
311 "properties": {
312 "name": {"type": "string"},
313 "service": {"type": "string"},
314 "path": {"type": "string"},
315 "path_type": {"type": "string", "enum": ["exact", "prefix", "regex"]}
316 },
317 "required": ["name", "service", "path"],
318 "additionalProperties": false
319 }),
320 required_permission: Permission::Write,
321 },
322 ToolDefinition {
323 name: "update_route".to_string(),
324 description: "Update an existing route".to_string(),
325 input_schema: serde_json::json!({
326 "type": "object",
327 "properties": {
328 "name": {"type": "string"},
329 "service": {"type": "string"},
330 "path": {"type": "string"}
331 },
332 "required": ["name"],
333 "additionalProperties": false
334 }),
335 required_permission: Permission::Write,
336 },
337 ToolDefinition {
338 name: "delete_route".to_string(),
339 description: "Delete a route by name".to_string(),
340 input_schema: serde_json::json!({
341 "type": "object",
342 "properties": {
343 "name": {"type": "string"}
344 },
345 "required": ["name"],
346 "additionalProperties": false
347 }),
348 required_permission: Permission::Write,
349 },
350 ToolDefinition {
351 name: "list_services".to_string(),
352 description: "List all services in the current config".to_string(),
353 input_schema: serde_json::json!({
354 "type": "object",
355 "properties": {},
356 "additionalProperties": false
357 }),
358 required_permission: Permission::Read,
359 },
360 ToolDefinition {
361 name: "get_stats".to_string(),
362 description: "Get gateway stats (counters, histograms)".to_string(),
363 input_schema: serde_json::json!({
364 "type": "object",
365 "properties": {},
366 "additionalProperties": false
367 }),
368 required_permission: Permission::Read,
369 },
370 ToolDefinition {
371 name: "get_health".to_string(),
372 description: "Get gateway health status".to_string(),
373 input_schema: serde_json::json!({
374 "type": "object",
375 "properties": {},
376 "additionalProperties": false
377 }),
378 required_permission: Permission::Read,
379 },
380 ToolDefinition {
381 name: "get_config".to_string(),
382 description: "Dump the current gateway config as YAML".to_string(),
383 input_schema: serde_json::json!({
384 "type": "object",
385 "properties": {},
386 "additionalProperties": false
387 }),
388 required_permission: Permission::Read,
389 },
390 ToolDefinition {
391 name: "purge_cache".to_string(),
392 description: "Purge the cache for a route or all routes".to_string(),
393 input_schema: serde_json::json!({
394 "type": "object",
395 "properties": {
396 "route": {"type": "string", "description": "The route name (or 'all')"}
397 },
398 "required": ["route"],
399 "additionalProperties": false
400 }),
401 required_permission: Permission::Admin,
402 },
403 ]
404}
405
406fn validate_arguments(schema: &Value, args: &Value) -> Result<(), String> {
413 let schema_obj = schema.as_object().ok_or("schema must be an object")?;
414
415 if let Some(schema_type) = schema_obj.get("type") {
417 if schema_type == "object" {
418 let args_obj = args.as_object().ok_or("arguments must be an object")?;
419
420 if let Some(required) = schema_obj.get("required").and_then(|r| r.as_array()) {
422 for field in required {
423 let field_name = field
424 .as_str()
425 .ok_or("required field name must be a string")?;
426 if !args_obj.contains_key(field_name) {
427 return Err(format!("missing required field: {field_name}"));
428 }
429 }
430 }
431
432 let additional_allowed = schema_obj
434 .get("additionalProperties")
435 .map(|v| v.as_bool().unwrap_or(true))
436 .unwrap_or(true);
437
438 if !additional_allowed {
439 if let Some(properties) = schema_obj.get("properties").and_then(|p| p.as_object()) {
440 for key in args_obj.keys() {
441 if !properties.contains_key(key) {
442 return Err(format!("unknown field: {key}"));
443 }
444 }
445 }
446 }
447 }
448 }
449
450 Ok(())
451}
452
453pub fn encode_sse_response(response: &Value) -> String {
463 format!("data: {response}\n\n")
464}
465
466pub fn encode_sse_error(id: Option<&Value>, code: i32, message: &str) -> String {
468 let error = serde_json::json!({
469 "jsonrpc": "2.0",
470 "id": id,
471 "error": {"code": code, "message": message}
472 });
473 encode_sse_response(&error)
474}
475
476#[derive(Debug, Clone, Copy, PartialEq, Eq)]
478pub enum McpTransport {
479 Http,
481 Sse,
483 Stdio,
485}
486
487impl McpTransport {
488 pub fn as_str(&self) -> &'static str {
490 match self {
491 McpTransport::Http => "http",
492 McpTransport::Sse => "sse",
493 McpTransport::Stdio => "stdio",
494 }
495 }
496
497 pub fn parse(s: Option<&str>) -> Self {
500 match s {
501 Some("sse") => McpTransport::Sse,
502 Some("stdio") => McpTransport::Stdio,
503 _ => McpTransport::Http,
504 }
505 }
506}