mcp-proxy 0.3.1

Standalone MCP proxy -- config-driven reverse proxy with auth, rate limiting, and observability
Documentation
# AGENTS.md

## Overview

mcp-proxy is a config-driven MCP (Model Context Protocol) reverse proxy built in Rust. It aggregates multiple MCP backends behind a single HTTP endpoint with per-backend middleware, authentication, and observability. Built on tower-mcp and the tower middleware ecosystem.

Package name: `mcp-proxy`. Binary name: `mcp-proxy`. Library name: `mcp_proxy`.

## Project structure

```
src/
  main.rs          # CLI entry point (clap), logging setup
  lib.rs           # Library root, re-exports Proxy and ProxyConfig
  proxy.rs         # Core: builds proxy, middleware stack, and axum router
  config.rs        # TOML config parsing, all config types
  admin.rs         # HTTP admin API (/admin/backends, /admin/health, etc.)
  admin_tools.rs   # MCP admin tools (proxy/ namespace, via ChannelTransport)
  reload.rs        # Hot reload: file watcher, dynamic backend addition
  test_util.rs     # MockService, ErrorMockService, call_service helper

  # Global middleware (wraps the entire proxy, applied in proxy.rs)
  access_log.rs    # Structured access logging (AccessLogService, target: mcp::access)
  alias.rs         # Tool renaming (AliasService)
  filter.rs        # Capability filtering -- allow/deny lists, glob patterns (CapabilityFilterService)
  inject.rs        # Argument injection into tool calls (InjectArgsService)
  mirror.rs        # Traffic mirroring to canary backends (MirrorService)
  cache.rs         # Response caching with TTL (CacheService)
  coalesce.rs      # Request deduplication (CoalesceService)
  validation.rs    # Argument size limits (ValidationService)
  metrics.rs       # Prometheus counters and histograms (MetricsService)
  rbac.rs          # Role-based access control (RbacService)
  token.rs         # Auth token passthrough to backends (TokenPassthroughService)

  # Per-backend middleware (applied per-backend in proxy.rs and reload.rs)
  retry.rs         # Retry with exponential backoff (McpRetryPolicy)
  outlier.rs       # Outlier detection and ejection (OutlierDetectionLayer)

tests/
  integration.rs   # Middleware composition tests with in-process backends via ChannelTransport
  e2e.rs           # Comprehensive E2E test suite (44 tests, 10 tiers)

examples/
  *.toml           # Example proxy configs for different deployment patterns
  docker-compose/  # Docker compose example with HTTP backend
```

## Architecture

### Middleware stack ordering

The middleware stack is built in `proxy.rs::build_middleware_stack()`. Order matters -- outermost runs first.

**Global middleware** (wraps the entire proxy service):
```
Request flow (outer to inner):
Auth (axum layer) -> Audit -> Access Log -> Metrics -> Token Passthrough -> RBAC
  -> Alias -> Filter -> Validation -> Coalesce -> Cache
  -> Mirror -> Inject Args -> McpProxy
```

**Per-backend middleware** (applied to each backend individually):
```
Request flow (inner to outer, applied via builder.backend_layer()):
Retry -> Hedge -> Concurrency Limit -> Rate Limit
  -> Timeout -> Circuit Breaker -> Outlier Detection -> Backend
```

### Key design pattern: Error = Infallible

All services use `Error = Infallible`. Errors are represented inside the response:
`RouterResponse { id: RequestId, inner: Result<McpResponse, JsonRpcError> }`.

tower-resilience and tower middleware produce typed errors (e.g., `CircuitBreakerError<E>`).
These are converted to `RouterResponse` error values via `tower_mcp::CatchError` wrapper.
The pattern in `reload.rs` is:
```rust
let limited = tower::Layer::layer(&layer, svc);
svc = BoxCloneService::new(tower_mcp::CatchError::new(limited));
```

In `proxy.rs`, `builder.backend_layer(layer)` handles this internally.

### Adding a new middleware layer

Every middleware follows the same tower Service pattern. Use `inject.rs` as a template:

1. Create `src/your_middleware.rs` with:
   - A service struct wrapping an inner service: `YourService<S> { inner: S, config: ... }`
   - `impl<S> Service<RouterRequest> for YourService<S>` with `Response = RouterResponse`, `Error = Infallible`
   - Match on `req.inner` to inspect the MCP request type (ListTools, CallTool, etc.)
   - Unit tests using `MockService` and `call_service` from `test_util`

2. Add `pub mod your_middleware;` to `lib.rs`

3. Wire it into the stack in `proxy.rs::build_middleware_stack()`:
   - For global middleware: wrap the `BoxCloneService` at the appropriate position
   - For per-backend: use `builder.backend_layer(layer)` in the backend loop

4. Add config fields to `config.rs` (with `#[serde(default)]` for optional fields)

5. Wire the hot reload path in `reload.rs::build_backend_layer()` (per-backend only)

6. Add integration tests in `tests/integration.rs`

### Adding a new backend transport

Transports are added in `proxy.rs::build_mcp_proxy()` and `reload.rs::add_backend()`:

1. Add a variant to `TransportType` enum in `config.rs`
2. Add a match arm in both `build_mcp_proxy()` and `add_backend()`
3. The transport must implement tower-mcp's `ClientTransport` trait

### Configuration

All config is in `config.rs`. The top-level type is `ProxyConfig`:
- `proxy`: name, version, separator, listen address, hot_reload
- `backends[]`: name, transport, command/url, per-backend middleware configs
- `auth`: bearer tokens or JWT/JWKS with RBAC
- `performance`: request coalescing
- `security`: argument size limits
- `observability`: logging, metrics, tracing

Config maps directly to runtime: each optional config section (timeout, rate_limit, circuit_breaker, etc.) gates whether that middleware layer is applied.

Environment variable substitution: `${VAR_NAME}` in string values is resolved via `config.resolve_env_vars()`.

### Testing

Every feature gets tests. No exceptions.

**Unit tests** (`#[cfg(test)]` in each module):
- Use `MockService::with_tools(&["tool1", "tool2"])` for a service that returns tool lists and echoes call results
- Use `ErrorMockService` for a service that returns JSON-RPC errors
- Use `call_service(&mut svc, McpRequest::...)` to send requests through the service

**E2E tests** (`tests/e2e.rs`):
- Comprehensive test suite covering full proxy pipeline
- Use `ChannelTransport` to create in-process MCP backends (no external processes)
- Build an `McpProxy` with tools, compose middleware, verify end-to-end behavior
- Includes error backends, slow backends, concurrent request testing
- Use `tower-resilience-chaos` for chaos engineering / failure injection tests

**Integration tests** (`tests/integration.rs`):
- Middleware composition tests with real proxy routing

### Documentation

Rust docs are the single source of truth for both CLI users and library users.

- All public APIs must have doc comments
- Module-level docs explain purpose and usage patterns
- Doc examples should be runnable (`cargo test --doc`)
- Keep examples up to date -- stale doc examples break `cargo test --doc`

**Pre-push checks** (MUST pass before every push):
```bash
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --lib --all-features
cargo test --test '*' --all-features
cargo doc --no-deps --all-features       # catches missing/broken docs
cargo test --doc --all-features          # catches stale doc examples
```

## Conventions

- Rust 2024 edition, MSRV 1.90
- `anyhow` for application errors, `thiserror` for library errors
- Conventional commits: `feat:`, `fix:`, `docs:`, `refactor:`, `test:`
- All public APIs have doc comments
- Metrics use `mcp_proxy_` prefix
- Admin tools live under `proxy/` namespace