Skip to content

feat: MCP stdio transport — Claude Code can talk to RAUTA#60

Merged
yairfalse merged 2 commits into
mainfrom
feat/mcp-stdio-transport
Mar 24, 2026
Merged

feat: MCP stdio transport — Claude Code can talk to RAUTA#60
yairfalse merged 2 commits into
mainfrom
feat/mcp-stdio-transport

Conversation

@yairfalse
Copy link
Copy Markdown
Collaborator

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_backend

Changes

  • mcp-server/src/handler.rsRautaMcpHandler implements rmcp::ServerHandler via #[tool_router]/#[tool_handler] macros. Each tool delegates to GatewayQuery trait.
  • rauta-cli — new mcp subcommand, RemoteGatewayQuery now implements GatewayQuery trait
  • agent-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

  • 220 tests pass, clippy clean, fmt clean
  • rauta mcp shows in rauta --help
  • Binary compiles and starts MCP handshake over stdio
  • Manual: configure in Claude Code settings, verify tools appear

🤖 Generated with Claude Code

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>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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::RautaMcpHandler implementing rmcp::ServerHandler with 11 MCP tools backed by GatewayQuery.
  • Adds rauta mcp CLI subcommand to run the MCP server over stdio, wiring it to a RemoteGatewayQuery.
  • Upgrades schemars from 0.8 → 1.x to satisfy rmcp requirements.

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.

Comment on lines +53 to 65
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)
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread rauta-cli/src/remote_query.rs Outdated
Comment on lines +82 to +86
Ok(vec![])
}

async fn cache_stats(&self) -> anyhow::Result<Option<CacheStats>> {
Ok(None)
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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)

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — cache_stats() now calls GET /api/v1/cache. list_listeners() extracts listeners from the snapshot (admin API includes them in status).

Comment on lines +67 to +94
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![])
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread mcp-server/src/handler.rs Outdated
//! Usage:
//! ```rust,ignore
//! let handler = RautaMcpHandler::new(query);
//! handler.serve(rmcp::transport::stdio()).await?;
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
//! handler.serve(rmcp::transport::stdio()).await?;
//! rmcp::ServiceExt::serve(handler, rmcp::transport::stdio()).await?;

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — doc example now shows rmcp::ServiceExt::serve(handler, rmcp::transport::stdio()) matching the actual API used in the CLI.

Comment thread mcp-server/src/handler.rs
Comment on lines +18 to +82
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>,
}

Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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`.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@yairfalse yairfalse merged commit 11f7631 into main Mar 24, 2026
2 checks passed
@yairfalse yairfalse deleted the feat/mcp-stdio-transport branch March 24, 2026 01:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants