# mcp-proxy [![Crates.io](https://img.shields.io/crates/v/mcp-proxy.svg)](https://crates.io/crates/mcp-proxy) [![docs.rs](https://docs.rs/mcp-proxy/badge.svg)](https://docs.rs/mcp-proxy) [![CI](https://github.com/joshrotenberg/mcp-proxy/actions/workflows/ci.yml/badge.svg)](https://github.com/joshrotenberg/mcp-proxy/actions/workflows/ci.yml) [![License](https://img.shields.io/crates/l/mcp-proxy.svg)](LICENSE-MIT) A Tower-native MCP traffic plane for teams that need resilient, policy-aware MCP routing without adopting a full platform. mcp-proxy is a config-driven [Model Context Protocol](https://modelcontextprotocol.io/) reverse proxy. It aggregates stdio, HTTP, and WebSocket MCP backends behind one endpoint and applies authentication, per-backend resilience, traffic management, and observability as composable middleware from the [tower](https://github.com/tower-rs/tower) ecosystem, via [tower-mcp](https://github.com/joshrotenberg/tower-mcp). Run it as a single binary or embed it in a Rust application. > **Project status:** maintained with an intentionally stable scope. mcp-proxy > is a tower-native gateway and reference deployment for aggregating MCP > backends. Maintenance focuses on security, dependency and protocol updates, > bug fixes, and documentation rather than speculative new features. ## Where it fits - **Self-hosted internal MCP fleets** -- one authenticated, observable endpoint in front of the MCP servers a team already runs. - **Non-Kubernetes and mixed deployments** -- a single binary and a TOML file; no service mesh, operator, or container platform required. - **Rust applications that need a gateway** -- the same proxy is a library; mount it in an existing axum app or drive it from a builder. For how mcp-proxy relates to other MCP gateways (Docker MCP Gateway, IBM ContextForge, Microsoft MCP Gateway, Kong), see [docs/comparison.md](docs/comparison.md). ## What it does **Aggregates many MCP servers behind one endpoint.** Backends speaking stdio, HTTP, or WebSocket are exposed under per-backend namespaces at a single HTTP endpoint. Tools, resources, and prompts can be allow/deny filtered and aliased per backend; default or per-tool arguments can be injected into calls; composite tools fan one call out across multiple backend tools; hot reload adds new backends from config changes without a restart; and optional BM25 discovery can expose search instead of full tool lists. **Contains backend failures.** Each backend gets its own resilience chain: timeouts, rate limits, concurrency caps, retries with exponential backoff and budgets, circuit breakers, request hedging, and outlier detection that temporarily ejects unhealthy backends. **Manages traffic for rollouts and load.** Traffic mirroring shadows a percentage of requests to a canary backend; canary routing and ordered failover control weighted rollouts; response caching (in-memory, Redis, or SQLite) and request coalescing cut duplicate work. **Applies policy at the front door.** Bearer token, JWT/JWKS, and OAuth 2.1 authentication; role-based tool visibility (RBAC); and request argument validation. Backend authentication uses separately configured credentials; inbound tokens are never forwarded. **Reports what is happening.** Prometheus metrics, OpenTelemetry trace export, structured audit logging, an admin HTTP API for health, backend status, and cache stats, and admin MCP tools under the `proxy/` namespace. **Development versus published versions:** this branch contains unreleased fixes. Installed artifacts and docs.rs/latest may differ; see the version guidance in the documentation map and build rustdoc locally for this checkout. Start with the [documentation map](docs/README.md). Configuration recipes are in [`config.example.toml`](config.example.toml), deployment shapes in [`docs/architectures.md`](docs/architectures.md), and runnable configurations in [`examples/`](examples/). ## Installation ### Homebrew ```bash brew install joshrotenberg/brew/mcp-proxy ``` ### Cargo ```bash cargo install mcp-proxy ``` ### Docker ```bash docker pull ghcr.io/joshrotenberg/mcp-proxy:latest docker run -v ./proxy.toml:/etc/mcp-proxy/proxy.toml:ro -p 8080:8080 ghcr.io/joshrotenberg/mcp-proxy:latest ``` ### Pre-built binaries Download from [GitHub Releases](https://github.com/joshrotenberg/mcp-proxy/releases). ## Quick Start Create a `proxy.toml`: ```toml [proxy] name = "my-proxy" separator = "/" [proxy.listen] host = "127.0.0.1" port = 8080 [[backends]] name = "files" transport = "stdio" command = "npx" args = ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] ``` This local example requires Node.js/npm (`npx`), package registry access and a filesystem directory the child can access. Backend packages are installed by `npx`, not by mcp-proxy. Keep the loopback binding for this unauthenticated example. Check the configuration, then run: ```bash mcp-proxy --config proxy.toml --check mcp-proxy --config proxy.toml ``` All tools from the filesystem server are now available under the `files/` namespace at `http://127.0.0.1:8080/`. The standalone MCP endpoint is `/`; external backends may expose their own endpoint at a different path, such as `/mcp`, which belongs in that backend's `url`. ## Configuration See [`config.example.toml`](config.example.toml) for an annotated configuration example; the [configuration rustdoc](https://docs.rs/mcp-proxy/latest/mcp_proxy/config/) owns the typed reference and limits. For a complete production example (ten backends behind one endpoint, with JWT/RBAC, per-backend resilience, a mirrored canary, selective caching, and metrics), see [docs/production-gateway.md](docs/production-gateway.md) and [`examples/production-gateway.toml`](examples/production-gateway.toml). Feature snippets below marked as fragments extend a config with the `[proxy]` and `[proxy.listen]` tables from Quick Start. Backend examples replace/add their `[[backends]]` block; do not concatenate duplicate backend names or tables. Authentication examples are complete configs with a placeholder HTTP backend. ### Per-backend middleware ```toml [[backends]] name = "github" transport = "stdio" command = "npx" args = ["-y", "@modelcontextprotocol/server-github"] [backends.env] GITHUB_PERSONAL_ACCESS_TOKEN = "${GITHUB_TOKEN}" [backends.timeout] seconds = 60 [backends.rate_limit] requests = 30 period_seconds = 1 [backends.circuit_breaker] failure_rate_threshold = 0.5 minimum_calls = 5 wait_duration_seconds = 30 [backends.retry] max_retries = 3 initial_backoff_ms = 100 max_backoff_ms = 5000 budget_percent = 20.0 [backends.hedging] delay_ms = 200 max_hedges = 1 [backends.outlier_detection] consecutive_errors = 5 base_ejection_seconds = 30 max_ejection_percent = 50 [backends.cache] tool_ttl_seconds = 60 resource_ttl_seconds = 300 ``` ### Argument injection ```toml [[backends]] name = "db" transport = "http" url = "http://db.internal:8080" # Inject into all tool calls for this backend [backends.default_args] timeout = 30 # Inject into a specific tool (overrides default_args for matching keys) [[backends.inject_args]] tool = "query" args = { read_only = true, max_rows = 1000 } # Force overwrite existing arguments [[backends.inject_args]] tool = "dangerous_op" args = { dry_run = true } overwrite = true ``` ### Traffic mirroring ```toml [[backends]] name = "api" transport = "http" url = "http://api-v1:8080" [[backends]] name = "api-v2" transport = "http" url = "http://api-v2:8080" mirror_of = "api" mirror_percent = 10 ``` ### Backend headers and tool discovery HTTP backends (including SSE responses) and WebSocket handshakes can use custom outbound headers. Values support the same environment substitution as bearer tokens: ```toml [[backends]] name = "api" transport = "http" url = "https://mcp.example.com" headers = { "X-API-Key" = "${API_KEY}" } ``` Environment references must occupy the whole value, such as `"${API_KEY}"`, using a variable name made of letters, digits, and underscores (not starting with a digit). Embedded forms such as `"Bearer ${API_KEY}"` are rejected. Resolution applies to backend `env`, `headers`, and `bearer_token`, inbound bearer/scoped tokens, `security.admin_token`, and OAuth `client_secret`. Missing, empty, non-Unicode, malformed, and recursively unresolved references fail startup and `--check`; diagnostics identify the configuration field without printing the reference or secret. Literal empty backend environment/header values remain valid. Use `config.prepare()?` for the shared offline runtime check; `resolve_env_vars()` remains a fallible compatible alias. `Proxy::from_config` (including `ProxyBuilder::build`), CLI check/startup, management and reload use this contract before backend effects. Unknown config fields, invalid regexes, reserved/ambiguous names, routing conflicts, unsafe numeric values and enabled features missing from the binary are errors. See [configuration rustdoc](https://docs.rs/mcp-proxy/latest/mcp_proxy/config/index.html#preparation-strict-fields-and-limits) for limits and migration. Parsing/loading permits unresolved templates; rejected management saves/reloads retain stored templates and active backends. An explicit `Authorization` header overrides `bearer_token` regardless of casing. Rust callers upgrading from 0.4 to 0.5 who construct `BackendConfig` literals need to add `headers: Default::default()`; TOML and YAML configs can omit the map. Invalid or duplicate header names, unknown backend fields, and headers configured on stdio backends fail configuration validation. With `[proxy] tool_exposure = "search"`, discover a tool through `proxy/search_tools`, pass its returned `id` to `proxy/get_tool` to retrieve its name, description, and complete input schema, then invoke it through `proxy/call_tool`. This keeps full schemas out of search result lists while making nested parameters and enums available before execution. `proxy/call_tool` authorizes the public **target name**, with the same bearer scope/JWT role checks, static filters, aliases, argument transforms, size limits, and caches as a direct call. Permission to the wrapper grants no target access; a caller permitted to invoke a target does not also need to allow the wrapper name. Identity, cancellation, progress, and continuation fields are preserved. Management and recursive `proxy/` targets are rejected (using your configured separator). Use the configured `Proxy` router; raw admin-tool registration cannot execute this wrapper. Composite calls require permission for the composite name **and every invoked target**, for both direct and wrapped calls. Each target uses its public name (including any alias) and the caller's policies and argument transformations. Denied targets receive no backend call; other permitted targets may run, and any target failure marks the aggregate as an error. Nested composites are not expanded. ### Bounded request coalescing Enable coalescing only when sharing identical concurrent tool calls and resource reads is appropriate for the deployment. It shares an in-flight result, not a cached result; later requests execute again. Each caller retains its request ID. ```toml [performance] coalesce_requests = true [performance.coalesce] max_entries = 1024 # Active distinct request keys max_waiters = 128 # Followers per key, excluding the leader timeout_seconds = 300 # Whole-group deadline, including backend readiness ``` These are the defaults for existing configurations and `CoalesceService::new`. Library users can customize them with `CoalesceService::with_config`. Capacities must be positive; timeout must be 1–86400 seconds. Excess active keys or followers receive a JSON-RPC capacity error without dispatching more work. Non-coalescible methods pass through; their timeouts remain a backend concern. Cancelling a follower frees its slot. Cancelling the leader cancels its local backend future, removes the group and wakes followers with an interruption error. A group deadline returns timeout errors and releases the slot. Backend JSON-RPC errors are shared like successful results. No automatic replay occurs on cancellation or expiry; remote execution or side effects may already have occurred. This adds a finite deadline to coalesced operations, including leaders; raise it explicitly for legitimately long operations. Changes to these limits require restart. ### MCP request logging ```toml [observability] log_level = "warn" # General application/dependency verbosity json_logs = true audit = true [observability.access_log] enabled = true ``` The CLI emits enabled audit (`mcp::audit`) and access (`mcp::access`) records at INFO independently of `log_level`. Each flag defaults to false and controls its own target. This applies to text, JSON and OpenTelemetry subscriber paths; records go to stderr. The CLI uses these configuration settings rather than `RUST_LOG`. Embedded library users configure their own tracing subscriber and must enable the desired targets at INFO. Access records contain method, target, backend when identifiable, duration and status (`ok`/`error`) and a bounded `outcome`. Audit records contain method, request ID, target, duration, status, outcome, numeric error code and annotation hints when available. Audit status is `success`, `denied` for invalid-params or forbidden errors, or `error` for other failures. Tool results with `isError=true` are errors in access logs, audit logs and request counters. Arguments, credential headers, response bodies and free-form backend error messages are omitted. Request IDs and tool/resource/prompt identifiers remain logged fields. Records are emitted when the MCP service completes. HTTP authentication rejections occur before this middleware and are not included; these records do not yet attribute callers. Request spans use OTLP HTTP/protobuf (`http://localhost:4318/v1/traces` by default) and include HTTP authentication rejections. Configured proxies own independent metrics recorders; embedders can inject a shared recorder. See the [telemetry guide](https://docs.rs/mcp-proxy/latest/mcp_proxy/telemetry/index.html) for provider shutdown/flush, bounded namespace labels and embedding migration. ### MCP outcome policy Resilience, cache admission and telemetry share a classifier based on typed results and numeric JSON-RPC codes. Error text and application error data do not change the policy. | Outcome (`outcome` label) | Retry / breaker / outlier failure | Configured hedge seeks another result | Cache | | --- | --- | --- | --- | | `success` | No | No | Completed tool results and resource reads only | | `tool_error` (`isError=true`) | No | Yes | No | | `client_error` | No | No | No | | `server_error` | Yes | Yes | No | | `transport_error` | Yes | Yes | No | | `other_error` | No | No | No | Client errors include invalid requests/params/methods, parse errors and known MCP resource/session/authorization/subscription/header/capability/version/elicitation codes, even when assigned in the `-32099..=-32000` range. Other codes in that range and internal error (`-32603`) count as server failures. Explicit connection closed (`-32000`), request timeout (`-32010`) and typed service failures count as transport failures. Unknown application codes remain terminal. The pinned upstream backend service folds some backend errors to `-32603`; their original code or transport type cannot be recovered, so those responses count as server failures without inspecting their messages. Retry and hedging require explicit configuration. A fast server or tool failure cannot beat a slower successful hedge; if every attempt fails, the original primary response retains its ID, error data and `isError` flag. Client/protocol and unknown errors are terminal hedge results. Retries and hedges can execute a tool more than once; enable them only where duplicate execution is acceptable. Failed management backend additions also return `isError=true`. Outlier admission and recovery now update fleet accounting atomically. Service clones and pending calls share one registration; final drop releases it. Recovery uses monotonic time and ignores stale completions from earlier ejection cycles. See [outlier rustdoc](https://docs.rs/mcp-proxy/latest/mcp_proxy/outlier/) for quota, owner lifetime and manual detector accounting semantics. Retry backoff now honors `max_backoff_ms` from the first delay. The optional percentage budget counts original backend invocations in ten-second fixed windows; the default low-traffic floor allows 100 reservations per window. Set `min_retries_per_sec = 0` for volume-only credit. This replaces the former fixed token bucket. Parsed configs reject over 1000 retries or five minutes of planned retry sleep; use the backend timeout to bound dispatched execution too. See [retry rustdoc](https://docs.rs/mcp-proxy/latest/mcp_proxy/retry/) for exact window, cancellation and migration semantics. Caches reject failed results both on writes and when reading entries created by older versions. Intermediate task/continuation responses are never cached. Migration: `mcp_proxy_requests_total` retains `method` and `status` and adds the bounded `outcome` label shown above. Update recording rules that expect an exact label set; use aggregation when retaining the previous series shape. Tool failures now contribute to `status="error"`. Access/audit records also add `outcome`. The public `admin::admin_router` breaker-handle map now uses `CircuitBreakerHandle`; callers constructing those handles must use that classifier rather than the default service-error-only classifier. Configuration fields are unchanged. ### Shutdown and embedding `[proxy].shutdown_timeout_seconds` (default 30) bounds HTTP draining after SIGTERM or SIGINT. The standalone server stops accepting, lets short requests finish, then cancels remaining connection and HTTP/2 stream tasks at the deadline, including pending tools and open SSE. Managed backend transports close next; health and reload workers are cancelled and joined. Transport cleanup runs concurrently with a 250 ms close bound per transport. Configured stdio children get 100 ms to exit after EOF, then are killed and reaped (with another 100 ms wait). This owns the direct child process; descendants launched independently are not managed as a process group. `Proxy`, its returned router clones, and retained `ShutdownHandle` clones share runtime ownership. Last-owner drop cancels workers and schedules bounded transport cleanup without blocking. For joined cleanup, keep a handle before embedding, stop accepting and drain your host's requests, then call it: ```rust,ignore let shutdown = proxy.shutdown_handle(); let (router, sessions) = proxy.into_router(); // Serve router in the host app, then stop/drain its requests. shutdown.shutdown().await?; ``` The handle closes MCP sessions and managed backends and rejects new requests through retained proxy routers with HTTP 503. It is idempotent across clones; other host routes/listeners remain owned by the host. `Proxy::shutdown().await` provides the same cleanup without embedding. An active Tokio runtime is required for asynchronous cleanup. Aborted operations are not replayed; remote side effects may already have occurred. Construction errors and cancellation also close already-created transports. Keep a proxy/router/shutdown owner while using a raw `mcp_proxy()` clone. Transports added directly through that trusted upstream handle bypass managed registration and remain the caller's responsibility. Low-level API migration: `admin::spawn_health_checker` now returns `(AdminState, HealthChecker)`; retain the second value or health checking stops. Use `HealthChecker::shutdown().await` to cancel even a pending ping and join its worker. Health-check/initialization time budgets remain separate work (#283). ### Supported hot reload changes The CLI watches its file when `proxy.hot_reload = true`. Library users call `Proxy::enable_hot_reload(path)`; repeated calls retain the first watcher. Live changes support adding, removing, renaming, or reconnecting **plain, unreferenced backends**. Their configurable connection fields are transport, command/arguments, URL, subprocess environment, bearer token, and headers. Stdio, HTTP and WebSocket drivers keep serving while the watcher waits. All global settings and auth changes require restart. Backend policies also require restart: filters, aliases, argument transforms, caching, forwarding, routing, timeout, retry, concurrency, rate limit, hedging, circuit breaker and outlier detection. A backend carrying any such policy, or referenced by routing or composites, cannot be replaced or removed live, even if only its URL changes. Startup remains the sole middleware constructor, preserving its active breaker handles and shared outlier fleet. Unsupported **whole revisions** are rejected before any backend mutation; the previous runtime remains active. `POST /admin/config/validate` and `PUT /admin/config` return HTTP 409 with `restart_required=true` for unsupported revisions; PUT does not write them. Accepted PUTs report `applied=false`: they save the original template and queue asynchronous reload, or report restart required if no watcher is active. Authenticated `GET /admin/config/status` reports `watcher_active`, `last_result`, `applied`, and `restart_required` for the last attempt. Results include `startup`, `pending`, `applied`, `invalid`, `restart_required`, `failed`, and `stopped`. A failed connection revision can still be partial; transactional replacement is separate work. GET `/admin/config` remains a redacted startup snapshot. Direct REST/MCP backend mutations also reject policy-bearing or referenced names. `Proxy` owns the watcher. `into_router()` transfers ownership to the router and its clones; final-owner drop cancels watching and its runtime. Standalone serving joins it after requests drain. Low-level `reload::spawn_config_watcher*` callers must retain the returned `ConfigWatcher` while using reloaded transports and ensure the initial file describes their constructed proxy. Dropping the handle cancels it; `shutdown().await` cancels and joins its worker thread. The configured Proxy captures its resolved startup policy independently of later file edits. ### OAuth resource identity and endpoint setup For JWT and OAuth authentication, `auth.audience` is the public MCP resource URL, for example `https://gateway.example.com/mcp`. It is used verbatim for both token validation and protected-resource metadata; the local listen address is not an OAuth identity. Tokens must contain that exact value in their `aud` string or string array. Logical labels such as `mcp-proxy` must be migrated to a public URL. Resource and issuer identities require HTTPS, except HTTP on loopback for local development, and cannot contain user information, queries, fragments, or whitespace. The path-aware RFC 9728 metadata endpoint is public. The example resource above advertises `https://gateway.example.com/.well-known/oauth-protected-resource/mcp`; route that path through your ingress as well as the MCP path. OAuth setup discovers only endpoints missing for the chosen strategy: `jwt` needs JWKS, `introspection` needs only introspection, and `both` needs both. Providing those endpoints skips discovery entirely. Discovery verifies the exact issuer and supports RFC 8414 issuer paths plus OIDC fallback. JWT, discovery, and introspection requests have five-second deadlines and refuse redirects; discovery/introspection bodies are limited to 64 KiB. Introspection preserves string/array audiences, rejects expired active responses and mismatched issuers, and binds an omitted optional `iss` to the configured trusted introspection issuer. Required scopes and credential-bound session ownership remain enforced. ### RBAC role policies JWT/OAuth RBAC requires both nonempty `auth.roles` and `auth.role_mapping`. Role names must be unique and nonempty, and every mapping must reference a defined role. Incomplete policies and mapping typos fail configuration validation. Library callers must now handle `RbacConfig::new(&roles, &mapping)` returning a `Result`. All mapped roles contribute grants, independent of scope/group order. An empty allow list grants every tool; an explicit deny in **any** mapped role overrides all grants, including an unrestricted admin role. For example, a reader plus a writer can use both allow lists, but a deny on deletion in either role still blocks deletion. Listings and calls use the same combined policy. Scopes are space-delimited. Custom claims accept a string (exact mapping first, otherwise space-delimited values) or a string array (exact element mappings). Custom introspection claims such as `groups` are preserved. An installed RBAC layer rejects missing verified claims, malformed custom claims, and unknown roles. Valid claims with no mapped values, including an absent role claim, follow `default_deny`; set it to `true` for gateways. Its legacy default of `false` permits those unmapped principals without RBAC restrictions. ### Upgrading to 0.6 Version 0.6 uses tower-mcp 0.23. Library applications that exchange `SessionHandle`, `McpProxy`, router requests/responses, or protocol types with mcp-proxy should update their direct tower-mcp dependencies to 0.23 as well. Development builds now use tower-mcp 0.24 from an exact Git revision for credential-safe diagnostics. Embedding applications must use the matching Git sources for both `tower-mcp` and `tower-mcp-types`; see the [dependency and release prerequisite](CONTRIBUTING.md#temporary-upstream-fix-and-release-prerequisite). This dependency change requires a new minor release before publication. HTTP backends follow same-origin redirects only. A redirect to another scheme, host, or port fails instead of forwarding custom headers or session identifiers; HTTPS-to-HTTP redirects also fail. Configure the backend's final URL directly if the previous deployment depended on a cross-origin redirect. JWT/OAuth-authenticated HTTP sessions carrying a `sub` claim are bound to that subject. Clients must initialize their own session and authenticate subsequent requests as the same subject. ### Authentication `forward_auth = true` is rejected. Use a separately configured backend credential; see the [backend authentication guide](https://docs.rs/mcp-proxy/latest/mcp_proxy/config/index.html#backend-authentication-and-migration) for migration, shared-cache identity and redirect boundaries. Bearer authentication (a complete configuration with a placeholder HTTP backend): ```toml [proxy] name = "example" [proxy.listen] host = "127.0.0.1" port = 8080 [[backends]] name = "files" transport = "http" url = "http://127.0.0.1:9000/mcp" [auth] type = "bearer" tokens = ["${API_TOKEN}"] ``` Alternatively, JWT with RBAC (`oauth` feature). Set `ADMIN_TOKEN` before checking or starting; the ordinary role called `admin` below grants backend tool access, while only the dedicated credential grants management authority. Replace the placeholder backend and configure the real issuer/JWKS endpoints before deployment. ```toml [proxy] name = "example" [proxy.listen] host = "127.0.0.1" port = 8080 [[backends]] name = "files" transport = "http" url = "http://127.0.0.1:9000/mcp" [auth] type = "jwt" issuer = "https://auth.example.com" audience = "https://mcp.example.com/mcp" jwks_uri = "https://auth.example.com/.well-known/jwks.json" [[auth.roles]] name = "reader" allow_tools = ["files/read_file", "files/list_directory"] [[auth.roles]] name = "admin" [auth.role_mapping] claim = "scope" mapping = { "mcp:read" = "reader", "mcp:admin" = "admin" } default_deny = true [security] admin_token = "${ADMIN_TOKEN}" ``` ### Capability filtering ```toml [[backends]] name = "files" transport = "stdio" command = "npx" args = ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] # Only expose these tools expose_tools = ["read_file", "list_directory"] # Or hide specific tools # hide_tools = ["write_file", "delete_file"] ``` ## Library Usage Add to your `Cargo.toml`: ```toml [dependencies] mcp-proxy = "0.6" ``` Inside an async function returning `anyhow::Result<()>`: ```rust use mcp_proxy::{Proxy, ProxyConfig}; let config = ProxyConfig::load("proxy.toml".as_ref())?; let proxy = Proxy::from_config(config).await?; // Serve standalone: proxy.serve().await?; ``` For embedding, use `proxy.into_router()` instead of consuming the same proxy with `serve()`. Retain a shutdown handle and drain the host server; see the [compiled embedding example](https://docs.rs/mcp-proxy/latest/mcp_proxy/#shutdown-and-embedding). ## Admin API With the default `openapi` feature, `/admin/openapi.json` describes every admin operation and requires the dedicated admin token. Its API version follows the package version; see the [OpenAPI contract guide](https://docs.rs/mcp-proxy/latest/mcp_proxy/admin/#openapi-contracts-and-versioning). Process probes at `GET /livez` and `GET /readyz` return `ok` without credentials. Readiness indicates startup completed; detailed backend health remains at the protected `/admin/health` endpoint. For Kubernetes deployment and release version maintenance, see the [Helm chart guide](charts/mcp-proxy/README.md). Multiple replicas need session affinity and client recovery after owner loss; Redis shares responses, not sessions. See the [session routing guide](https://docs.rs/mcp-proxy/latest/mcp_proxy/#multiple-replicas-and-sessions) and the tested [NGINX configuration](examples/nginx-session-affinity.conf). Management requires a dedicated credential, distinct from ordinary and scoped bearer tokens: ```toml [security] admin_token = "${ADMIN_TOKEN}" ``` Send `Authorization: Bearer ` to `/admin/*` or initialize a separate MCP session at `/` with that credential. Admin MCP sessions expose management tools, prompts, and resources; use ordinary credentials for backend operations and discovery helpers. Without `security.admin_token`, management is disabled, including in otherwise unauthenticated local deployments. Bearer-token fallback has been removed. JWT/OAuth configs continue to require an explicit admin token. Sessions are bound to their credential identity and management authority. Start a new session when switching static tokens or between management and ordinary credentials; a session ID alone cannot grant access, stream another caller's events, or terminate their session. OAuth sessions retain their issuer/subject identity across token refreshes (or issuer/client ID for client credentials). Authenticated OAuth tokens with neither identity cannot initialize a session. Management runs outside data-plane caching, coalescing, RBAC, and capability transforms. Both paths retain configured audit/access logs, metrics, and the shared global rate limit. Ordinary requests retain their backend policies, and cannot reach management indirectly through composites or `proxy/call_tool`. HTTP endpoints: - `GET /admin/backends` -- list backends with health status and proxy info - `GET /admin/health` -- health check summary (healthy/degraded) - `GET /admin/metrics` -- Prometheus metrics - `GET /admin/cache/stats` -- per-backend cache hit/miss rates - `POST /admin/cache/clear` -- clear all caches MCP tools (under `proxy/` namespace): - `proxy/list_backends` -- list backends with health status - `proxy/health_check` -- cached health check results - `proxy/session_count` -- active session count - `proxy/add_backend` -- dynamically add an HTTP backend - `proxy/config` -- redacted configuration diagnostics `GET /admin/config`, `proxy/config`, and the `proxy/check_config` and `proxy/explain_config` prompts share an allowlisted diagnostic view. Credentials, URLs, headers, environment values, commands, subprocess arguments, injected and default argument values, and other non-allowlisted fields appear as `[REDACTED]`, including for administrators. New fields stay redacted until explicitly reviewed. Proxy/backend names, transports, and resilience settings remain visible. This is a startup snapshot for diagnosis, not a configuration backup or an editable export. Use your original configuration file and secret source for updates; sending the redacted view to `PUT /admin/config` does not restore hidden values. Check private values locally when validating or explaining a snapshot. Configuration parser errors report positions without quoting input lines or rejected values. Backend management responses omit transport error details and URL values, which may contain credentials. OAuth discovery, introspection, and JWKS startup diagnostics also omit endpoint values and untrusted response data. ## Architecture The [crate-level middleware order](https://docs.rs/mcp-proxy/latest/mcp_proxy/#middleware-order) is authoritative for request flow, response unwinding and the management branch. Global layers wrap the proxy; each backend owns its resilience chain. For an application-owned decision service, see [external tool gates](docs/external-tool-gates.md) for interception points, policy ordering, and a bounded non-production pilot. ## Feature Flags Pre-built binaries and `cargo install` include the default features. If you're building from source and don't need everything, you can disable optional features for a smaller binary: | Feature | Default | What it includes | |---------|---------|-----------------| | `otel` | yes | OpenTelemetry request spans (OTLP HTTP/protobuf export) | | `metrics` | yes | Prometheus metrics and `/admin/metrics` endpoint | | `oauth` | yes | JWT/JWKS auth, OAuth, bearer scoping, and RBAC | | `openapi` | yes | OpenAPI schema and endpoint support | | `websocket` | yes | WebSocket backend transport | | `discovery` | yes | BM25 tool discovery and search exposure mode | | `yaml` | yes | YAML configuration files using the maintained `yaml_serde` parser | | `skills` | yes | agentskills.io prompts for proxy administration | | `redis-cache` | no | Redis response cache; explicit generations enable response sharing | | `sqlite-cache` | no | SQLite response cache; explicit generations enable restart reuse | | `protocol-2026-07-28` | no | Released MCP 2026-07-28 protocol support through tower-mcp | ```bash # Minimal build (bearer auth only, no metrics/tracing/JWT) cargo install mcp-proxy --no-default-features # Just metrics, no otel or JWT cargo install mcp-proxy --no-default-features --features metrics ``` Config parsing always works regardless of features -- if you reference a disabled feature in your config (e.g., `type = "jwt"` without the `oauth` feature), you'll get a clear error at startup. The default protocol baseline remains MCP 2025-11-25. Build with `--features protocol-2026-07-28` to compile support for the released, sessionless 2026-07-28 protocol. Continuation fields such as `inputResponses` and `requestState` are preserved through proxy routing in either build. ## License Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or [MIT license](LICENSE-MIT) at your option.