diff --git a/agent-api/Cargo.toml b/agent-api/Cargo.toml index 579e924..14fa89a 100644 --- a/agent-api/Cargo.toml +++ b/agent-api/Cargo.toml @@ -7,7 +7,7 @@ description = "RAUTA agent API: types, query trait, and diagnostics engine" [dependencies] serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -schemars = "0.8" +schemars = "1" async-trait = "0.1" anyhow = "1.0" diff --git a/mcp-server/Cargo.toml b/mcp-server/Cargo.toml index 11c1452..390bdd4 100644 --- a/mcp-server/Cargo.toml +++ b/mcp-server/Cargo.toml @@ -6,9 +6,10 @@ description = "RAUTA MCP server for AI agent integration" [dependencies] agent-api = { path = "../agent-api" } +rmcp = { version = "1.2", features = ["server", "macros", "transport-io"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -schemars = "0.8" +schemars = "1" tokio = { version = "1", features = ["full"] } async-trait = "0.1" anyhow = "1.0" diff --git a/mcp-server/src/handler.rs b/mcp-server/src/handler.rs new file mode 100644 index 0000000..d36c831 --- /dev/null +++ b/mcp-server/src/handler.rs @@ -0,0 +1,284 @@ +//! MCP Server Handler (rmcp) +//! +//! Implements `rmcp::ServerHandler` for RAUTA gateway tools. +//! Wraps any `GatewayQuery` implementation — `LocalGatewayQuery` for in-process +//! access, `RemoteGatewayQuery` for HTTP-based access. +//! +//! Usage: +//! ```rust,ignore +//! let handler = RautaMcpHandler::new(query); +//! rmcp::ServiceExt::serve(handler, rmcp::transport::stdio()).await?; +//! ``` + +use agent_api::query::GatewayQuery; +use rmcp::handler::server::router::tool::ToolRouter; +use rmcp::handler::server::wrapper::Parameters; +use rmcp::model::{CallToolResult, Content, ServerCapabilities, ServerInfo}; +use rmcp::{tool, tool_handler, tool_router, ErrorData as McpError, ServerHandler}; +use schemars::JsonSchema; +use serde::Deserialize; +use std::sync::Arc; + +// ============================================================================ +// Parameter types for MCP tools +// These use rmcp's schemars (1.x) for schema generation in tool definitions +// ============================================================================ + +#[derive(Debug, Deserialize, JsonSchema)] +pub struct ListRoutesParams { + /// Filter by HTTP method (GET, POST, etc.) + pub method: Option, + /// Filter by path prefix + pub path_prefix: Option, +} + +#[derive(Debug, Deserialize, JsonSchema)] +pub struct GetRouteParams { + /// Route pattern to look up + pub pattern: String, +} + +#[derive(Debug, Deserialize, JsonSchema)] +pub struct ListCircuitBreakersParams { + /// Filter by state: Open, Closed, or HalfOpen + pub state: Option, +} + +#[derive(Debug, Deserialize, JsonSchema)] +pub struct ListRateLimitersParams { + /// Filter by route pattern + pub route: Option, +} + +#[derive(Debug, Deserialize, JsonSchema)] +pub struct DiagnoseParams { + /// Symptom to diagnose (e.g., "high-latency", "circuit-breaker-cascade") + pub symptom: String, + /// Filter by route pattern + pub route: Option, + /// Filter by backend address + pub backend: Option, +} + +#[derive(Debug, Deserialize, JsonSchema)] +pub struct DrainBackendParams { + /// Backend address to drain (e.g., "10.0.1.5:8080") + pub backend: String, + /// Drain timeout in seconds (default: 30) + pub timeout: Option, +} + +#[derive(Debug, Deserialize, JsonSchema)] +pub struct UndrainBackendParams { + /// Backend address to undrain + pub backend: String, +} + +#[derive(Debug, Deserialize, JsonSchema)] +pub struct MetricsSnapshotParams { + /// Filter by metric name + pub metric: Option, +} + +// ============================================================================ +// MCP Handler +// ============================================================================ + +/// RAUTA MCP server handler +/// +/// Each `#[tool]` method maps to a `GatewayQuery` method. +/// rmcp generates JSON Schema from the parameter types and handles +/// JSON-RPC framing over stdio. +#[derive(Clone)] +pub struct RautaMcpHandler { + query: Arc, + #[allow(dead_code)] + tool_router: ToolRouter, +} + +#[tool_router] +impl RautaMcpHandler { + pub fn new(query: Arc) -> Self { + Self { + query, + tool_router: Self::tool_router(), + } + } + + #[tool( + description = "Get gateway health overview: uptime, route count, open circuits, rate limiter status" + )] + async fn rauta_status(&self) -> Result { + let snapshot = self + .query + .snapshot() + .await + .map_err(|e| McpError::internal_error(e.to_string(), None))?; + let json = serde_json::to_string_pretty(&snapshot) + .map_err(|e| McpError::internal_error(e.to_string(), None))?; + Ok(CallToolResult::success(vec![Content::text(json)])) + } + + #[tool(description = "List all configured routes with backends, filters, and health status")] + async fn rauta_list_routes( + &self, + Parameters(params): Parameters, + ) -> Result { + let routes = self + .query + .list_routes(params.method.as_deref(), params.path_prefix.as_deref()) + .await + .map_err(|e| McpError::internal_error(e.to_string(), None))?; + let json = serde_json::to_string_pretty(&routes) + .map_err(|e| McpError::internal_error(e.to_string(), None))?; + Ok(CallToolResult::success(vec![Content::text(json)])) + } + + #[tool(description = "Get detailed information about a single route")] + async fn rauta_get_route( + &self, + Parameters(params): Parameters, + ) -> Result { + let route = self + .query + .get_route(¶ms.pattern) + .await + .map_err(|e| McpError::internal_error(e.to_string(), None))?; + let json = serde_json::to_string_pretty(&route) + .map_err(|e| McpError::internal_error(e.to_string(), None))?; + Ok(CallToolResult::success(vec![Content::text(json)])) + } + + #[tool(description = "List circuit breaker states for all backends")] + async fn rauta_list_circuit_breakers( + &self, + Parameters(params): Parameters, + ) -> Result { + let breakers = self + .query + .list_circuit_breakers(params.state.as_deref()) + .await + .map_err(|e| McpError::internal_error(e.to_string(), None))?; + let json = serde_json::to_string_pretty(&breakers) + .map_err(|e| McpError::internal_error(e.to_string(), None))?; + Ok(CallToolResult::success(vec![Content::text(json)])) + } + + #[tool(description = "List rate limiter states showing tokens available and capacity")] + async fn rauta_list_rate_limiters( + &self, + Parameters(params): Parameters, + ) -> Result { + let limiters = self + .query + .list_rate_limiters(params.route.as_deref()) + .await + .map_err(|e| McpError::internal_error(e.to_string(), None))?; + let json = serde_json::to_string_pretty(&limiters) + .map_err(|e| McpError::internal_error(e.to_string(), None))?; + Ok(CallToolResult::success(vec![Content::text(json)])) + } + + #[tool( + description = "Run diagnostic rules to detect gateway issues. Returns structured diagnoses with causal chains and suggested actions" + )] + async fn rauta_diagnose( + &self, + Parameters(params): Parameters, + ) -> Result { + let diagnoses = self + .query + .diagnose( + ¶ms.symptom, + params.route.as_deref(), + params.backend.as_deref(), + ) + .await + .map_err(|e| McpError::internal_error(e.to_string(), None))?; + let json = serde_json::to_string_pretty(&diagnoses) + .map_err(|e| McpError::internal_error(e.to_string(), None))?; + Ok(CallToolResult::success(vec![Content::text(json)])) + } + + #[tool(description = "Get route cache statistics (hit rate, size)")] + async fn rauta_cache_stats(&self) -> Result { + let stats = self + .query + .cache_stats() + .await + .map_err(|e| McpError::internal_error(e.to_string(), None))?; + let json = serde_json::to_string_pretty(&stats) + .map_err(|e| McpError::internal_error(e.to_string(), None))?; + Ok(CallToolResult::success(vec![Content::text(json)])) + } + + #[tool(description = "List active network listeners with protocols and Gateway references")] + async fn rauta_list_listeners(&self) -> Result { + let listeners = self + .query + .list_listeners() + .await + .map_err(|e| McpError::internal_error(e.to_string(), None))?; + let json = serde_json::to_string_pretty(&listeners) + .map_err(|e| McpError::internal_error(e.to_string(), None))?; + Ok(CallToolResult::success(vec![Content::text(json)])) + } + + #[tool(description = "Get Prometheus metrics as structured JSON")] + async fn rauta_metrics_snapshot( + &self, + Parameters(params): Parameters, + ) -> Result { + let metrics = self + .query + .metrics_snapshot(params.metric.as_deref()) + .await + .map_err(|e| McpError::internal_error(e.to_string(), None))?; + let json = serde_json::to_string_pretty(&metrics) + .map_err(|e| McpError::internal_error(e.to_string(), None))?; + Ok(CallToolResult::success(vec![Content::text(json)])) + } + + #[tool( + description = "Gracefully drain a backend, preventing new requests while allowing existing connections to finish" + )] + async fn rauta_drain_backend( + &self, + Parameters(params): Parameters, + ) -> Result { + self.query + .drain_backend(¶ms.backend, params.timeout) + .await + .map_err(|e| McpError::internal_error(e.to_string(), None))?; + let json = serde_json::json!({"status": "draining", "backend": params.backend}); + Ok(CallToolResult::success(vec![Content::text( + json.to_string(), + )])) + } + + #[tool(description = "Cancel drain for a backend, restoring it to active service")] + async fn rauta_undrain_backend( + &self, + Parameters(params): Parameters, + ) -> Result { + self.query + .undrain_backend(¶ms.backend) + .await + .map_err(|e| McpError::internal_error(e.to_string(), None))?; + let json = serde_json::json!({"status": "active", "backend": params.backend}); + Ok(CallToolResult::success(vec![Content::text( + json.to_string(), + )])) + } +} + +#[tool_handler] +impl ServerHandler for RautaMcpHandler { + fn get_info(&self) -> ServerInfo { + ServerInfo::new(ServerCapabilities::builder().enable_tools().build()).with_instructions( + "RAUTA AI-native Kubernetes API gateway. Query routes, backends, \ + circuit breakers, rate limiters, and run diagnostics." + .to_string(), + ) + } +} diff --git a/mcp-server/src/lib.rs b/mcp-server/src/lib.rs index b8ca3de..06a5c4c 100644 --- a/mcp-server/src/lib.rs +++ b/mcp-server/src/lib.rs @@ -30,4 +30,4 @@ //! The MCP server wraps a `GatewayQuery` implementation. In-process, this is //! `LocalGatewayQuery`. For remote access, `RemoteGatewayQuery` (from rauta-cli). -pub mod tools; +pub mod handler; diff --git a/mcp-server/src/tools.rs b/mcp-server/src/tools.rs deleted file mode 100644 index 0386305..0000000 --- a/mcp-server/src/tools.rs +++ /dev/null @@ -1,386 +0,0 @@ -//! MCP Tool Definitions -//! -//! Each tool corresponds to a `GatewayQuery` method. Tool definitions include -//! JSON Schema for parameters and return types (via `schemars`). - -use agent_api::query::GatewayQuery; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::sync::Arc; - -/// MCP tool definition -#[derive(Debug, Clone, Serialize)] -pub struct McpToolDef { - pub name: &'static str, - pub description: &'static str, - pub input_schema: serde_json::Value, -} - -/// Parameters for rauta_list_routes tool -#[derive(Debug, Deserialize, JsonSchema)] -pub struct ListRoutesParams { - /// Filter by HTTP method (GET, POST, etc.) - pub method: Option, - /// Filter by path prefix - pub path_prefix: Option, -} - -/// Parameters for rauta_get_route tool -#[derive(Debug, Deserialize, JsonSchema)] -pub struct GetRouteParams { - /// Route pattern to look up - pub pattern: String, -} - -/// Parameters for rauta_list_circuit_breakers tool -#[derive(Debug, Deserialize, JsonSchema)] -pub struct ListCircuitBreakersParams { - /// Filter by state (Open, Closed, HalfOpen) - pub state: Option, -} - -/// Parameters for rauta_list_rate_limiters tool -#[derive(Debug, Deserialize, JsonSchema)] -pub struct ListRateLimitersParams { - /// Filter by route pattern - pub route: Option, -} - -/// Parameters for rauta_diagnose tool -#[derive(Debug, Deserialize, JsonSchema)] -pub struct DiagnoseParams { - /// Symptom to diagnose (e.g., "high-latency", "circuit-breaker-cascade") - pub symptom: String, - /// Filter by route pattern - pub route: Option, - /// Filter by backend address - pub backend: Option, -} - -/// Parameters for rauta_drain_backend tool -#[derive(Debug, Deserialize, JsonSchema)] -pub struct DrainBackendParams { - /// Backend address to drain (e.g., "10.0.1.5:8080") - pub backend: String, - /// Drain timeout in seconds (default: 30) - pub timeout: Option, -} - -/// Parameters for rauta_undrain_backend tool -#[derive(Debug, Deserialize, JsonSchema)] -pub struct UndrainBackendParams { - /// Backend address to undrain - pub backend: String, -} - -/// Parameters for rauta_metrics_snapshot tool -#[derive(Debug, Deserialize, JsonSchema)] -pub struct MetricsSnapshotParams { - /// Filter by metric name - pub metric: Option, -} - -/// MCP tool executor backed by a GatewayQuery implementation -pub struct McpToolExecutor { - query: Arc, -} - -impl McpToolExecutor { - pub fn new(query: Arc) -> Self { - Self { query } - } - - /// List all available MCP tools with their schemas - pub fn list_tools(&self) -> Vec { - vec![ - McpToolDef { - name: "rauta_status", - description: "Get gateway health overview: uptime, route count, open circuits, rate limiter status", - input_schema: serde_json::json!({"type": "object", "properties": {}}), - }, - McpToolDef { - name: "rauta_list_routes", - description: "List all configured routes with backends, filters, and health status", - input_schema: serde_json::json!({ - "type": "object", - "properties": { - "method": {"type": "string", "description": "Filter by HTTP method"}, - "path_prefix": {"type": "string", "description": "Filter by path prefix"} - } - }), - }, - McpToolDef { - name: "rauta_get_route", - description: "Get detailed information about a single route", - input_schema: serde_json::json!({ - "type": "object", - "properties": { - "pattern": {"type": "string", "description": "Route pattern to look up"} - }, - "required": ["pattern"] - }), - }, - McpToolDef { - name: "rauta_list_circuit_breakers", - description: "List circuit breaker states for all backends", - input_schema: serde_json::json!({ - "type": "object", - "properties": { - "state": {"type": "string", "description": "Filter by state (Open, Closed, HalfOpen)"} - } - }), - }, - McpToolDef { - name: "rauta_list_rate_limiters", - description: "List rate limiter states showing tokens available and capacity", - input_schema: serde_json::json!({ - "type": "object", - "properties": { - "route": {"type": "string", "description": "Filter by route pattern"} - } - }), - }, - McpToolDef { - name: "rauta_diagnose", - description: "Run diagnostic rules to detect gateway issues. Returns structured diagnoses with causal chains and suggested actions", - input_schema: serde_json::json!({ - "type": "object", - "properties": { - "symptom": {"type": "string", "description": "Symptom to diagnose"}, - "route": {"type": "string", "description": "Filter by route"}, - "backend": {"type": "string", "description": "Filter by backend"} - }, - "required": ["symptom"] - }), - }, - McpToolDef { - name: "rauta_cache_stats", - description: "Get route cache statistics (hit rate, size)", - input_schema: serde_json::json!({"type": "object", "properties": {}}), - }, - McpToolDef { - name: "rauta_list_listeners", - description: "List active network listeners with protocols and Gateway references", - input_schema: serde_json::json!({"type": "object", "properties": {}}), - }, - McpToolDef { - name: "rauta_drain_backend", - description: "Gracefully drain a backend, preventing new requests while allowing existing connections to finish", - input_schema: serde_json::json!({ - "type": "object", - "properties": { - "backend": {"type": "string", "description": "Backend address (e.g., 10.0.1.5:8080)"}, - "timeout": {"type": "integer", "description": "Drain timeout in seconds (default: 30)"} - }, - "required": ["backend"] - }), - }, - McpToolDef { - name: "rauta_undrain_backend", - description: "Cancel drain for a backend, restoring it to active service", - input_schema: serde_json::json!({ - "type": "object", - "properties": { - "backend": {"type": "string", "description": "Backend address to undrain"} - }, - "required": ["backend"] - }), - }, - McpToolDef { - name: "rauta_metrics_snapshot", - description: "Get Prometheus metrics as structured JSON", - input_schema: serde_json::json!({ - "type": "object", - "properties": { - "metric": {"type": "string", "description": "Filter by metric name"} - } - }), - }, - ] - } - - /// Execute an MCP tool by name with JSON parameters - pub async fn call_tool( - &self, - name: &str, - params: serde_json::Value, - ) -> Result { - match name { - "rauta_status" => { - let snapshot = self.query.snapshot().await.map_err(|e| e.to_string())?; - serde_json::to_value(snapshot).map_err(|e| e.to_string()) - } - "rauta_list_routes" => { - let p: ListRoutesParams = - serde_json::from_value(params).map_err(|e| e.to_string())?; - let routes = self - .query - .list_routes(p.method.as_deref(), p.path_prefix.as_deref()) - .await - .map_err(|e| e.to_string())?; - serde_json::to_value(routes).map_err(|e| e.to_string()) - } - "rauta_get_route" => { - let p: GetRouteParams = - serde_json::from_value(params).map_err(|e| e.to_string())?; - let route = self - .query - .get_route(&p.pattern) - .await - .map_err(|e| e.to_string())?; - serde_json::to_value(route).map_err(|e| e.to_string()) - } - "rauta_list_circuit_breakers" => { - let p: ListCircuitBreakersParams = - serde_json::from_value(params).map_err(|e| e.to_string())?; - let cbs = self - .query - .list_circuit_breakers(p.state.as_deref()) - .await - .map_err(|e| e.to_string())?; - serde_json::to_value(cbs).map_err(|e| e.to_string()) - } - "rauta_list_rate_limiters" => { - let p: ListRateLimitersParams = - serde_json::from_value(params).map_err(|e| e.to_string())?; - let rls = self - .query - .list_rate_limiters(p.route.as_deref()) - .await - .map_err(|e| e.to_string())?; - serde_json::to_value(rls).map_err(|e| e.to_string()) - } - "rauta_diagnose" => { - let p: DiagnoseParams = - serde_json::from_value(params).map_err(|e| e.to_string())?; - let diagnoses = self - .query - .diagnose(&p.symptom, p.route.as_deref(), p.backend.as_deref()) - .await - .map_err(|e| e.to_string())?; - serde_json::to_value(diagnoses).map_err(|e| e.to_string()) - } - "rauta_cache_stats" => { - let stats = self.query.cache_stats().await.map_err(|e| e.to_string())?; - serde_json::to_value(stats).map_err(|e| e.to_string()) - } - "rauta_list_listeners" => { - let listeners = self - .query - .list_listeners() - .await - .map_err(|e| e.to_string())?; - serde_json::to_value(listeners).map_err(|e| e.to_string()) - } - "rauta_drain_backend" => { - let p: DrainBackendParams = - serde_json::from_value(params).map_err(|e| e.to_string())?; - self.query - .drain_backend(&p.backend, p.timeout) - .await - .map_err(|e| e.to_string())?; - Ok(serde_json::json!({"status": "draining", "backend": p.backend})) - } - "rauta_undrain_backend" => { - let p: UndrainBackendParams = - serde_json::from_value(params).map_err(|e| e.to_string())?; - self.query - .undrain_backend(&p.backend) - .await - .map_err(|e| e.to_string())?; - Ok(serde_json::json!({"status": "active", "backend": p.backend})) - } - "rauta_metrics_snapshot" => { - let p: MetricsSnapshotParams = - serde_json::from_value(params).map_err(|e| e.to_string())?; - let metrics = self - .query - .metrics_snapshot(p.metric.as_deref()) - .await - .map_err(|e| e.to_string())?; - serde_json::to_value(metrics).map_err(|e| e.to_string()) - } - _ => Err(format!("Unknown tool: {}", name)), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use agent_api::types::*; - - #[test] - fn test_tool_list_has_all_tools() { - // Use a mock query (we just need the tool definitions, not actual execution) - struct MockQuery; - - #[async_trait::async_trait] - impl GatewayQuery for MockQuery { - async fn snapshot(&self) -> anyhow::Result { - unimplemented!() - } - async fn list_routes( - &self, - _: Option<&str>, - _: Option<&str>, - ) -> anyhow::Result> { - unimplemented!() - } - async fn get_route(&self, _: &str) -> anyhow::Result> { - unimplemented!() - } - async fn list_circuit_breakers( - &self, - _: Option<&str>, - ) -> anyhow::Result> { - unimplemented!() - } - async fn list_rate_limiters( - &self, - _: Option<&str>, - ) -> anyhow::Result> { - unimplemented!() - } - async fn list_listeners(&self) -> anyhow::Result> { - unimplemented!() - } - async fn cache_stats(&self) -> anyhow::Result> { - unimplemented!() - } - async fn metrics_snapshot( - &self, - _: Option<&str>, - ) -> anyhow::Result> { - unimplemented!() - } - async fn diagnose( - &self, - _: &str, - _: Option<&str>, - _: Option<&str>, - ) -> anyhow::Result> { - unimplemented!() - } - async fn drain_backend(&self, _: &str, _: Option) -> anyhow::Result<()> { - unimplemented!() - } - async fn undrain_backend(&self, _: &str) -> anyhow::Result<()> { - unimplemented!() - } - } - - let executor = McpToolExecutor::new(Arc::new(MockQuery)); - let tools = executor.list_tools(); - - assert_eq!(tools.len(), 11, "Should have 11 MCP tools"); - - let tool_names: Vec<&str> = tools.iter().map(|t| t.name).collect(); - assert!(tool_names.contains(&"rauta_status")); - assert!(tool_names.contains(&"rauta_list_routes")); - assert!(tool_names.contains(&"rauta_diagnose")); - assert!(tool_names.contains(&"rauta_drain_backend")); - assert!(tool_names.contains(&"rauta_undrain_backend")); - assert!(tool_names.contains(&"rauta_metrics_snapshot")); - } -} diff --git a/rauta-cli/Cargo.toml b/rauta-cli/Cargo.toml index ec9077e..a19727b 100644 --- a/rauta-cli/Cargo.toml +++ b/rauta-cli/Cargo.toml @@ -14,6 +14,8 @@ path = "src/main.rs" [dependencies] agent-api = { path = "../agent-api" } +mcp-server = { path = "../mcp-server" } +rmcp = { version = "1.2", features = ["server", "transport-io"] } clap = { version = "4", features = ["derive", "env"] } comfy-table = "7" serde = { version = "1.0", features = ["derive"] } @@ -22,3 +24,5 @@ tokio = { version = "1", features = ["full"] } reqwest = { version = "0.12", features = ["json"] } async-trait = "0.1" anyhow = "1.0" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/rauta-cli/src/main.rs b/rauta-cli/src/main.rs index 3753483..4ac24a1 100644 --- a/rauta-cli/src/main.rs +++ b/rauta-cli/src/main.rs @@ -69,6 +69,9 @@ enum Commands { #[arg(long)] route: Option, }, + + /// Start MCP server over stdio (for Claude Code / Cursor integration) + Mcp, } #[derive(Subcommand)] @@ -169,6 +172,36 @@ async fn main() -> anyhow::Result<()> { let diagnoses = client.diagnose(&symptom).await?; output::render_diagnoses(&diagnoses, &cli.format); } + Commands::Mcp => { + // MCP server over stdio — stdout is the protocol channel, logs go to stderr + tracing_subscriber::fmt() + .with_writer(std::io::stderr) + .with_ansi(false) + .with_env_filter( + tracing_subscriber::EnvFilter::from_default_env() + .add_directive(tracing::Level::INFO.into()), + ) + .init(); + + tracing::info!("Starting RAUTA MCP server (stdio transport)"); + tracing::info!("Admin endpoint: {}", cli.endpoint); + + let query: std::sync::Arc = + std::sync::Arc::new(remote_query::RemoteGatewayQuery::new(&cli.endpoint)); + let handler = mcp_server::handler::RautaMcpHandler::new(query); + + let service = rmcp::ServiceExt::serve(handler, rmcp::transport::stdio()) + .await + .map_err(|e| anyhow::anyhow!("MCP serve error: {}", e))?; + + tracing::info!("MCP server running — waiting for client"); + service + .waiting() + .await + .map_err(|e| anyhow::anyhow!("MCP wait error: {}", e))?; + + return Ok(()); + } } Ok(()) diff --git a/rauta-cli/src/remote_query.rs b/rauta-cli/src/remote_query.rs index edfcbbd..c3404a1 100644 --- a/rauta-cli/src/remote_query.rs +++ b/rauta-cli/src/remote_query.rs @@ -1,8 +1,11 @@ //! Remote Gateway Query //! //! HTTP client that talks to the RAUTA admin server REST API. +//! Implements `GatewayQuery` trait so it can be used with the MCP handler. -use agent_api::types::{Diagnosis, GatewaySnapshot, RouteSnapshot}; +use agent_api::query::GatewayQuery; +use agent_api::types::*; +use async_trait::async_trait; pub struct RemoteGatewayQuery { base_url: String, @@ -17,28 +20,100 @@ impl RemoteGatewayQuery { } } + // Convenience methods for direct CLI use (without going through the trait) + pub async fn get_status(&self) -> anyhow::Result { + self.snapshot().await + } + + pub async fn get_routes( + &self, + method_filter: Option<&str>, + ) -> anyhow::Result> { + self.list_routes(method_filter, None).await + } + + pub async fn get_route(&self, pattern: &str) -> anyhow::Result> { + GatewayQuery::get_route(self, pattern).await + } + + pub async fn diagnose(&self, symptom: &str) -> anyhow::Result> { + GatewayQuery::diagnose(self, symptom, None, None).await + } +} + +#[async_trait] +impl GatewayQuery for RemoteGatewayQuery { + async fn snapshot(&self) -> anyhow::Result { let url = format!("{}/api/v1/status", self.base_url); let resp = self.client.get(&url).send().await?.error_for_status()?; - let snapshot: GatewaySnapshot = resp.json().await?; - Ok(snapshot) + Ok(resp.json().await?) } - pub async fn get_routes( + async fn list_routes( &self, - _method_filter: Option<&str>, + method_filter: Option<&str>, + path_prefix: Option<&str>, ) -> anyhow::Result> { let url = format!("{}/api/v1/routes", self.base_url); let resp = self.client.get(&url).send().await?.error_for_status()?; - let routes: Vec = resp.json().await?; + let mut routes: Vec = resp.json().await?; + + // Apply filters client-side (admin API returns all routes) + if let Some(method) = method_filter { + let m = method.to_uppercase(); + routes.retain(|r| r.method == m); + } + if let Some(prefix) = path_prefix { + routes.retain(|r| r.pattern.starts_with(prefix)); + } + Ok(routes) } - pub async fn get_route(&self, _pattern: &str) -> anyhow::Result> { - Ok(None) + async fn get_route(&self, pattern: &str) -> anyhow::Result> { + let routes = self.list_routes(None, None).await?; + Ok(routes.into_iter().find(|r| r.pattern == pattern)) } - pub async fn diagnose(&self, symptom: &str) -> anyhow::Result> { + async fn list_circuit_breakers( + &self, + _state_filter: Option<&str>, + ) -> anyhow::Result> { + anyhow::bail!("Circuit breaker listing not available via remote query — admin API endpoint not yet implemented") + } + + async fn list_rate_limiters( + &self, + _route_filter: Option<&str>, + ) -> anyhow::Result> { + anyhow::bail!("Rate limiter listing not available via remote query — admin API endpoint not yet implemented") + } + + async fn list_listeners(&self) -> anyhow::Result> { + let snapshot = self.snapshot().await?; + Ok(snapshot.listeners) + } + + async fn cache_stats(&self) -> anyhow::Result> { + let url = format!("{}/api/v1/cache", self.base_url); + let resp = self.client.get(&url).send().await?.error_for_status()?; + Ok(resp.json().await?) + } + + async fn metrics_snapshot( + &self, + _metric_filter: Option<&str>, + ) -> anyhow::Result> { + anyhow::bail!("Metrics snapshot not available via remote query — admin API endpoint not yet implemented") + } + + async fn diagnose( + &self, + symptom: &str, + _route_filter: Option<&str>, + _backend_filter: Option<&str>, + ) -> anyhow::Result> { let url = format!("{}/api/v1/diagnose", self.base_url); let resp = self .client @@ -47,7 +122,18 @@ impl RemoteGatewayQuery { .send() .await? .error_for_status()?; - let diagnoses: Vec = resp.json().await?; - Ok(diagnoses) + Ok(resp.json().await?) + } + + async fn drain_backend( + &self, + _backend: &str, + _timeout_secs: Option, + ) -> anyhow::Result<()> { + anyhow::bail!("drain_backend not yet implemented via remote query") + } + + async fn undrain_backend(&self, _backend: &str) -> anyhow::Result<()> { + anyhow::bail!("undrain_backend not yet implemented via remote query") } }