feat: MCP stdio transport — Claude Code can talk to RAUTA#60
Conversation
Add rmcp-based MCP server handler with stdio transport:
- RautaMcpHandler implements rmcp::ServerHandler via #[tool_router]/#[tool_handler]
- 11 MCP tools: rauta_status, rauta_list_routes, rauta_diagnose, etc.
- Each tool delegates to GatewayQuery trait (transport-agnostic)
- `rauta mcp` subcommand starts MCP server over stdio
- Connects to running gateway via admin REST API (RemoteGatewayQuery)
- Logs to stderr (stdout is MCP protocol channel)
RemoteGatewayQuery now implements GatewayQuery trait (was ad-hoc methods).
Upgraded schemars 0.8 → 1.x across workspace (rmcp 1.2 requires it).
Claude Code configuration:
```json
{
"mcpServers": {
"rauta": {
"command": "./target/release/rauta",
"args": ["mcp", "--endpoint", "http://localhost:9091"]
}
}
}
```
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds an rmcp-based MCP server (stdio transport) so AI clients (Claude Code / Cursor) can query and operate a running RAUTA gateway via the admin REST API.
Changes:
- Introduces
mcp-server::handler::RautaMcpHandlerimplementingrmcp::ServerHandlerwith 11 MCP tools backed byGatewayQuery. - Adds
rauta mcpCLI subcommand to run the MCP server over stdio, wiring it to aRemoteGatewayQuery. - Upgrades
schemarsfrom 0.8 → 1.x to satisfyrmcprequirements.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| rauta-cli/src/remote_query.rs | Implements GatewayQuery for the CLI’s remote admin API client, enabling MCP handler reuse. |
| rauta-cli/src/main.rs | Adds Mcp subcommand that starts the MCP server over stdio and logs to stderr. |
| rauta-cli/Cargo.toml | Adds mcp-server, rmcp, and tracing dependencies needed by the new subcommand. |
| mcp-server/src/lib.rs | Exposes the new handler module publicly. |
| mcp-server/src/handler.rs | New rmcp tool-router based MCP handler mapping tool calls to GatewayQuery. |
| mcp-server/Cargo.toml | Adds rmcp dependency and bumps schemars to 1.x. |
| agent-api/Cargo.toml | Bumps schemars to 1.x for shared schema derives. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| async fn list_routes( | ||
| &self, | ||
| _method_filter: Option<&str>, | ||
| _path_prefix: Option<&str>, | ||
| ) -> anyhow::Result<Vec<RouteSnapshot>> { | ||
| let url = format!("{}/api/v1/routes", self.base_url); | ||
| let resp = self.client.get(&url).send().await?.error_for_status()?; | ||
| let routes: Vec<RouteSnapshot> = resp.json().await?; | ||
| Ok(routes) | ||
| Ok(resp.json().await?) | ||
| } | ||
|
|
||
| pub async fn get_route(&self, _pattern: &str) -> anyhow::Result<Option<RouteSnapshot>> { | ||
| async fn get_route(&self, _pattern: &str) -> anyhow::Result<Option<RouteSnapshot>> { | ||
| Ok(None) | ||
| } |
There was a problem hiding this comment.
list_routes currently ignores method_filter/path_prefix, and get_route always returns Ok(None). This makes the CLI routes get and MCP tools like rauta_get_route effectively non-functional even though the admin API exposes /api/v1/routes. Consider implementing get_route by fetching /api/v1/routes and selecting by pattern, and applying the filters client-side in list_routes (until the admin API supports server-side filtering).
There was a problem hiding this comment.
Fixed in latest push — list_routes() now applies method/path filters client-side after fetching from the admin API. get_route() fetches all routes and selects by pattern match.
| Ok(vec![]) | ||
| } | ||
|
|
||
| async fn cache_stats(&self) -> anyhow::Result<Option<CacheStats>> { | ||
| Ok(None) |
There was a problem hiding this comment.
list_listeners and cache_stats are stubbed to always return an empty list / None, but the REST API already serves cache stats via GET /api/v1/cache and GatewaySnapshot includes listeners + cache_stats. This will cause MCP tools (rauta_list_listeners, rauta_cache_stats) to silently return no data even when the gateway has it. Consider implementing these by calling /api/v1/cache and/or reusing snapshot() to populate listeners/cache stats.
| Ok(vec![]) | |
| } | |
| async fn cache_stats(&self) -> anyhow::Result<Option<CacheStats>> { | |
| Ok(None) | |
| // Reuse the existing snapshot() call to fetch listeners from the remote gateway. | |
| let snapshot = self.snapshot().await?; | |
| Ok(snapshot.listeners) | |
| } | |
| async fn cache_stats(&self) -> anyhow::Result<Option<CacheStats>> { | |
| // Reuse the existing snapshot() call to fetch cache statistics from the remote gateway. | |
| let snapshot = self.snapshot().await?; | |
| Ok(snapshot.cache_stats) |
There was a problem hiding this comment.
Fixed — cache_stats() now calls GET /api/v1/cache. list_listeners() extracts listeners from the snapshot (admin API includes them in status).
| async fn list_circuit_breakers( | ||
| &self, | ||
| _state_filter: Option<&str>, | ||
| ) -> anyhow::Result<Vec<CircuitBreakerSnapshot>> { | ||
| Ok(vec![]) | ||
| } | ||
|
|
||
| async fn list_rate_limiters( | ||
| &self, | ||
| _route_filter: Option<&str>, | ||
| ) -> anyhow::Result<Vec<RateLimiterSnapshot>> { | ||
| Ok(vec![]) | ||
| } | ||
|
|
||
| async fn list_listeners(&self) -> anyhow::Result<Vec<ListenerSnapshot>> { | ||
| Ok(vec![]) | ||
| } | ||
|
|
||
| async fn cache_stats(&self) -> anyhow::Result<Option<CacheStats>> { | ||
| Ok(None) | ||
| } | ||
|
|
||
| async fn metrics_snapshot( | ||
| &self, | ||
| _metric_filter: Option<&str>, | ||
| ) -> anyhow::Result<Vec<MetricSnapshot>> { | ||
| Ok(vec![]) | ||
| } |
There was a problem hiding this comment.
Several GatewayQuery methods are implemented as unconditional empty results (list_circuit_breakers, list_rate_limiters, metrics_snapshot). For MCP, this is indistinguishable from “no circuit breakers / no rate limiters / no metrics” and can lead to incorrect agent decisions. If the admin API doesn’t expose these yet, consider returning a clear "not supported via remote query" error (like drain_backend does) or extending the admin API with dedicated endpoints.
There was a problem hiding this comment.
Fixed — list_circuit_breakers, list_rate_limiters, and metrics_snapshot now return explicit errors ("not available via remote query — admin API endpoint not yet implemented") instead of silent empty results.
| //! Usage: | ||
| //! ```rust,ignore | ||
| //! let handler = RautaMcpHandler::new(query); | ||
| //! handler.serve(rmcp::transport::stdio()).await?; |
There was a problem hiding this comment.
The usage example in the module docs calls handler.serve(...), but the CLI uses rmcp::ServiceExt::serve(handler, ...). Since this snippet is likely to be copy/pasted, consider updating it to match the actual rmcp API used in this repo to avoid confusion.
| //! handler.serve(rmcp::transport::stdio()).await?; | |
| //! rmcp::ServiceExt::serve(handler, rmcp::transport::stdio()).await?; |
There was a problem hiding this comment.
Fixed — doc example now shows rmcp::ServiceExt::serve(handler, rmcp::transport::stdio()) matching the actual API used in the CLI.
| 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<String>, | ||
| /// Filter by path prefix | ||
| pub path_prefix: Option<String>, | ||
| } | ||
|
|
||
| #[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<String>, | ||
| } | ||
|
|
||
| #[derive(Debug, Deserialize, JsonSchema)] | ||
| pub struct ListRateLimitersParams { | ||
| /// Filter by route pattern | ||
| pub route: Option<String>, | ||
| } | ||
|
|
||
| #[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<String>, | ||
| /// Filter by backend address | ||
| pub backend: Option<String>, | ||
| } | ||
|
|
||
| #[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<u64>, | ||
| } | ||
|
|
||
| #[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<String>, | ||
| } | ||
|
|
There was a problem hiding this comment.
This file redefines MCP parameter structs (ListRoutesParams, GetRouteParams, etc.) that already exist in mcp-server/src/tools.rs. Keeping two copies increases the risk that schemas/descriptions drift over time. Consider reusing the existing types (or removing/deprecating the old tools module) so there is a single source of truth for tool parameter schemas.
| 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<String>, | |
| /// Filter by path prefix | |
| pub path_prefix: Option<String>, | |
| } | |
| #[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<String>, | |
| } | |
| #[derive(Debug, Deserialize, JsonSchema)] | |
| pub struct ListRateLimitersParams { | |
| /// Filter by route pattern | |
| pub route: Option<String>, | |
| } | |
| #[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<String>, | |
| /// Filter by backend address | |
| pub backend: Option<String>, | |
| } | |
| #[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<u64>, | |
| } | |
| #[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<String>, | |
| } | |
| use std::sync::Arc; | |
| use crate::tools::{ | |
| DiagnoseParams, | |
| DrainBackendParams, | |
| GetRouteParams, | |
| ListCircuitBreakersParams, | |
| ListRateLimitersParams, | |
| ListRoutesParams, | |
| MetricsSnapshotParams, | |
| UndrainBackendParams, | |
| }; | |
| // ============================================================================ | |
| // Parameter types for MCP tools | |
| // Reuse definitions from the `tools` module so there is a single source of | |
| // truth for MCP tool parameter schemas. | |
| // ============================================================================ | |
| // Parameter structs (ListRoutesParams, GetRouteParams, ListCircuitBreakersParams, | |
| // ListRateLimitersParams, DiagnoseParams, DrainBackendParams, | |
| // UndrainBackendParams, MetricsSnapshotParams) are imported from `crate::tools`. |
There was a problem hiding this comment.
Resolved — removed tools.rs entirely. handler.rs is now the single source of truth for tool parameter schemas (using rmcp's #[tool_router] macros with schemars for schema generation).
- RemoteGatewayQuery.list_routes() now applies method/path filters client-side - RemoteGatewayQuery.get_route() fetches all routes and filters by pattern - RemoteGatewayQuery.cache_stats() calls GET /api/v1/cache - RemoteGatewayQuery.list_listeners() extracts from snapshot - Unsupported methods (list_circuit_breakers, list_rate_limiters, metrics_snapshot) return explicit errors instead of silent empty results - Fix handler.rs doc example to match actual rmcp API - Remove duplicate tools.rs (superseded by handler.rs with rmcp macros) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Adds rmcp-based MCP server so Claude Code (and Cursor) can query and operate a live RAUTA gateway.
New command:
rauta mcp— starts MCP server over stdio, connects to running gateway via admin REST API.11 MCP tools available to AI agents:
rauta_status,rauta_list_routes,rauta_get_route,rauta_list_circuit_breakers,rauta_list_rate_limiters,rauta_diagnose,rauta_cache_stats,rauta_list_listeners,rauta_metrics_snapshot,rauta_drain_backend,rauta_undrain_backendChanges
mcp-server/src/handler.rs—RautaMcpHandlerimplementsrmcp::ServerHandlervia#[tool_router]/#[tool_handler]macros. Each tool delegates toGatewayQuerytrait.rauta-cli— newmcpsubcommand,RemoteGatewayQuerynow implementsGatewayQuerytraitagent-api+mcp-server— upgraded schemars 0.8 → 1.x (rmcp 1.2 requires it)Claude Code configuration
{ "mcpServers": { "rauta": { "command": "./target/release/rauta", "args": ["mcp", "--endpoint", "http://localhost:9091"] } } }Test plan
rauta mcpshows inrauta --help🤖 Generated with Claude Code