mcp-proxy
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 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 ecosystem, via 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.
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. Configuration recipes are in config.example.toml, deployment shapes in docs/architectures.md, and runnable configurations in examples/.
Installation
Homebrew
brew install joshrotenberg/brew/mcp-proxy
Cargo
cargo install mcp-proxy
Docker
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.
Quick Start
Create a proxy.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:
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 for an annotated configuration example; the configuration rustdoc 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 and 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
[[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
[[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
[[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:
[[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
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.
[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
[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
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.
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 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 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<mcp_proxy::outcome::McpFailureClassifier>; 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:
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.
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
for migration, shared-cache identity and redirect boundaries.
Bearer authentication (a complete configuration with a placeholder HTTP backend):
[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.
[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
[[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:
[dependencies]
mcp-proxy = "0.6"
Inside an async function returning anyhow::Result<()>:
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.
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.
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.
Multiple replicas need session affinity and client recovery after owner loss;
Redis shares responses, not sessions. See the
session routing guide
and the tested NGINX configuration.
Management requires a dedicated credential, distinct from ordinary and scoped bearer tokens:
[security]
admin_token = "${ADMIN_TOKEN}"
Send Authorization: Bearer <ADMIN_TOKEN> 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 infoGET /admin/health-- health check summary (healthy/degraded)GET /admin/metrics-- Prometheus metricsGET /admin/cache/stats-- per-backend cache hit/miss ratesPOST /admin/cache/clear-- clear all caches
MCP tools (under proxy/ namespace):
proxy/list_backends-- list backends with health statusproxy/health_check-- cached health check resultsproxy/session_count-- active session countproxy/add_backend-- dynamically add an HTTP backendproxy/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 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 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:
# 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 or MIT license at your option.