Complete reference for MCPProxy configuration file (mcp_config.json). This document covers all configuration options, their defaults, and usage examples.
- Configuration File Location
- Basic Configuration
- Server Configuration
- Profiles
- Security Settings
- Tokenizer Configuration
- TLS/HTTPS Configuration
- Logging Configuration
- Docker Isolation
- Docker Recovery
- Environment Configuration
- Code Execution
- Feature Flags
- Registries
- Update Check
- Complete Example
MCPProxy looks for configuration in these locations (in order):
| OS | Config Location |
|---|---|
| macOS | ~/.mcpproxy/mcp_config.json |
| Windows | %USERPROFILE%\.mcpproxy\mcp_config.json |
| Linux | ~/.mcpproxy/mcp_config.json |
Note: At first launch, MCPProxy automatically generates a minimal configuration file if none exists.
A running MCPProxy core watches mcp_config.json and hot-reloads external edits automatically — whether written in place (echo ... > mcp_config.json) or atomically (jq ... > tmp && mv tmp mcp_config.json, the pattern most editors use). Behavior details:
- Edits are debounced for ~500 ms, so rapid write bursts collapse into a single reload.
- Invalid JSON is rejected safely: the running configuration is kept unchanged (a warning is logged) and the watcher picks up the next valid write.
- MCPProxy's own saves (Web UI, REST
PATCH /api/v1/config, CLI commands) do not trigger a redundant second reload. - Restart-required fields (e.g.
listen,data_dir) are reloaded into memory but only take effect after a restart.
{
"listen": "127.0.0.1:8080"
}| Field | Type | Default | Description |
|---|---|---|---|
listen |
string | "127.0.0.1:8080" |
Network address to bind to. Use :8080 for all interfaces, 127.0.0.1:8080 for localhost only (recommended for security) |
Examples:
"127.0.0.1:8080"- Localhost only (default, secure)":8080"- All network interfaces (use with caution)"0.0.0.0:9000"- All interfaces on port 9000
{
"data_dir": "~/.mcpproxy"
}| Field | Type | Default | Description |
|---|---|---|---|
data_dir |
string | "~/.mcpproxy" |
Directory for database and certificates. Supports ~ expansion for home directory. Logs use OS log directories unless log_dir is set |
{
"enable_socket": true,
"tray_endpoint": ""
}| Field | Type | Default | Description |
|---|---|---|---|
enable_socket |
boolean | true |
Enable Unix socket (macOS/Linux) or named pipe (Windows) for secure local IPC between tray and core |
tray_endpoint |
string | "" |
Override socket/pipe path (advanced, usually not needed) |
{
"tools_limit": 15,
"tool_response_limit": 20000,
"call_tool_timeout": "2m",
"init_timeout": "30s"
}| Field | Type | Default | Description |
|---|---|---|---|
tools_limit |
integer | 15 |
Maximum number of tools to return per request (1-1000) |
tool_response_limit |
integer | 20000 |
Maximum characters in tool responses (0 = unlimited). Applies to built-in tools too: an oversize retrieve_tools result is truncated and the full payload is retrievable via read_cache (except on the code-execution surface, which does not expose read_cache) |
call_tool_timeout |
string | "2m" |
Timeout for tool calls (e.g., "30s", "2m", "5m"). Note: When using agents like Codex or Claude as MCP servers, you may need to increase this timeout significantly, even up to 10 minutes ("10m"), as these agents may require longer processing times for complex operations |
init_timeout |
duration | "30s" |
Deadline for an upstream's MCP initialize handshake (e.g. "30s", "120s", "3m"). Raise this for servers that do legitimate first-run warmup — building a cache/index or prefetching — before they answer initialize, so they are not killed mid-startup. Global default; can be overridden per server (see Server Fields). Range: 1s–30m; "0s"/unset uses the 30s default. |
Deadlines applied to mcpproxy's own HTTP listener (REST API, /mcp, /events).
These are separate from call_tool_timeout, which caps how long an upstream
tool may run.
{
"http_read_timeout": "120s",
"http_write_timeout": "120s",
"http_idle_timeout": "180s"
}| Field | Type | Default | Description |
|---|---|---|---|
http_read_timeout |
duration | "120s" |
Deadline for reading the entire request (headers + body). "0s" = no timeout. Range: 1s–24h. |
http_write_timeout |
duration | "120s" |
Wall-clock cap on writing the entire response, counted from when the request headers were read. Governs non-streaming endpoints only (REST API, Web UI, health) — the MCP endpoints and the SSE /events stream are exempt by design (see below, #965). "0s" disables it globally. Range: 1s–24h. |
http_idle_timeout |
duration | "180s" |
Keep-alive timeout for idle persistent connections. "0s" removes the dedicated idle deadline, but Go's net/http then falls back to the read timeout — idle is fully unbounded only when http_read_timeout is also "0s". Range: 1s–24h. |
Notes:
- Streaming routes are exempt from
http_write_timeout. A write deadline caps the whole response, so it would truncate any tool call slower than it and silently kill long-lived SSE streams. The MCP endpoints (/mcp,/mcp/all,/mcp/code,/mcp/call,/mcp/p/<slug>, plus the legacy/v1/tool_codeand/v1/tool-codealiases) and/eventstherefore clear their own per-request write deadline (and, being body-less GETs, their read deadline). Everything else keeps the configured deadline, which is what protects a non-loopback deployment from slow readers. You do not need to disablehttp_write_timeoutto run long tool calls (#965). "0s"means "no timeout", not "use the default" — unlikeinit_timeout. Omit the key entirely to get the built-in default. Settinghttp_write_timeoutto"0s"removes the deadline from every endpoint, including REST/UI/health. Exception:http_idle_timeout: "0s"alone does not unbound idle connections — Go'snet/httpfalls back to the read timeout (see the field row above).- A restart is required. These values are baked into the HTTP server when it binds, so a config edit is reported as restart-required rather than hot-reloaded.
- Slowloris protection is unaffected: the 60s request-header read deadline is hardcoded and not configurable.
- Long tool calls need
call_tool_timeout. It (default"2m") separately caps tool execution. To allow tool calls longer than two minutes, raisecall_tool_timeout— the MCP routes' write-deadline exemption alone is not enough.
Environment overrides: MCPPROXY_HTTP_READ_TIMEOUT, MCPPROXY_HTTP_WRITE_TIMEOUT, MCPPROXY_HTTP_IDLE_TIMEOUT.
{
"toon_output": "adaptive",
"toon_min_savings_pct": 15
}| Field | Type | Default | Description |
|---|---|---|---|
toon_output |
string | "off" |
TOON encoding of call_tool_* result text blocks: off (byte-identical to pre-feature behavior), adaptive (encode only tabular-uniform JSON when the complete TOON emission — marker + decode hint + body — beats the passthrough by at least toon_min_savings_pct; never larger by construction), or always (encode every JSON-parseable block regardless of size — benchmarking/debugging only, can increase token cost). Hot-reloads; applies to the next tool call without restart. |
toon_min_savings_pct |
integer | 15 |
Minimum byte savings (percent, 1–90) the TOON emission must achieve over the passthrough for adaptive mode to encode. Byte savings approximate token savings for this payload class. |
Per-server override: set toon_output on a server entry to override the global for that server's tools (precedence: per-server > global > default off). See Server Fields and TOON Output for the full feature description (marker contract, safety chain, decision metadata).
mcpproxy keeps each upstream connection alive and its tool index fresh with two background loops. Both intervals are tunable globally and per server, so you can quiet a chatty upstream that returns a large tool catalog.
{
"health_check_interval": "30s",
"tool_discovery_interval": "5m"
}| Field | Type | Default | Description |
|---|---|---|---|
health_check_interval |
duration | "30s" |
How often to probe each connected server for liveness with a lightweight MCP ping. "0s" disables the periodic probe. Range: 5s–1h. Does not apply to Docker-isolated servers (see note below). |
tool_discovery_interval |
duration | "5m" |
How often to re-list every server's tools to rebuild the search index. "0s" disables the periodic sweep. Range: 30s–24h. Applies to all server types, including Docker. |
Docker-isolated servers.
health_check_intervalhas no effect on Docker-isolated servers. Their liveness is monitored separately at the container level on a fixed internal cadence (not an MCPping), so the periodic ping probe is intentionally skipped for them.tool_discovery_intervalstill applies to Docker servers. Remote (HTTP/SSE) servers benefit most from thepingswitch, since both the probe and the formertools/listcrossed the network.
Liveness uses ping, not tools/list. The health-check loop issues the
MCP-standard ping request rather than re-listing every tool, so an idle proxy
no longer generates large recurring tools/list traffic to upstream servers
(GitHub #608). Tool
changes are still picked up reactively whenever a server pushes
notifications/tools/list_changed.
Disabling a loop ("0s"). Set either key to "0s" to turn the
corresponding loop off:
health_check_interval: "0s"— no periodic liveness probe. A dead transport is then detected lazily, on the next real tool call or discovery sweep, rather than proactively.tool_discovery_interval: "0s"— no periodic index rebuild. Tools are still discovered at connect time and whenever a server pushesnotifications/tools/list_changed. Trade-off: a server that does not supportlist_changedwill not have new/removed tools reflected until it reconnects or you trigger a manual refresh.
An unset key behaves exactly as before this feature (the built-in default), and a change to either interval takes effect on the next cycle without restarting the proxy.
Per-server override. Both keys can also be set on an individual server entry under
mcpServers[](see Server Fields) to override the global value for just that server; the per-server value wins, and"0s"disables the loop for that server only. A dedicated per-server form control in the Web UI / macOS app is planned; for now set per-server overrides via the Raw JSON editor or the REST API.
Multi-user or multi-agent deployments can overwhelm a fragile upstream (a database-backed stdio server, a rate-limited API) with simultaneous tool calls. MCPProxy can cap how many upstream tool calls run at once and park the excess in a bounded FIFO queue, shedding predictably when that queue is full (GitHub #955).
Everything is off by default — with no keys set, behavior is exactly as before: no limiting, no queueing, no new errors.
There are three separately named scopes, each carrying the same three settings:
| Scope | Where | What it caps |
|---|---|---|
| Global aggregate limiter | top-level max_concurrent_requests / queue_size / queue_timeout |
All upstream tool calls across the whole proxy |
| Per-server default set | server_concurrency_defaults object |
Blanket per-server values, inherited by every server that does not override them |
| Per-server override | the same three keys on an mcpServers[] entry |
That one server |
{
"max_concurrent_requests": 50,
"queue_size": 100,
"queue_timeout": "30s",
"server_concurrency_defaults": {
"max_concurrent_requests": 5,
"queue_size": 10,
"queue_timeout": "30s"
},
"mcpServers": [
{ "name": "fragile-db", "command": "db-mcp", "max_concurrent_requests": 1, "queue_size": 2 },
{ "name": "fast-api", "url": "https://api.example.com/mcp", "max_concurrent_requests": 0 }
]
}| Field | Type | Default | Description |
|---|---|---|---|
max_concurrent_requests |
integer | unset (off) | Maximum upstream tool calls running at once in this scope. 0 (or unset) = no limiter for this scope. |
queue_size |
integer | 0 |
How many calls may wait for a slot. 0 = no pending capacity: a call arriving at the cap is shed immediately. |
queue_timeout |
duration | "30s" when a limiter is active |
How long a call may wait in the queue before being shed. |
Tri-state per-server semantics. Each per-server key is independently tri-state:
- absent — inherit the value from
server_concurrency_defaults; 0— disable that setting for this server.max_concurrent_requests: 0opts the server out of per-server limiting entirely (even when the default set configures one);queue_size: 0keeps the cap but removes the queue, so excess calls are shed instantly;- positive — override the default for this server.
The global aggregate limiter is never an inheritance source for a server. It applies on top: a server's effective concurrency is min(resolved per-server limit, global limit). Waiting for a per-server slot does not consume global capacity — the per-server slot is taken first.
One deadline, not two. queue_timeout is a total wait budget, not a
per-tier one: a call waiting for a per-server slot and then a global slot shares
a single absolute deadline (the smallest configured timeout among the active
scopes). Queue waiting never consumes the call's execution timeout — the
execution budget starts after admission.
Recommended starting point for stdio upstreams: 5. stdio does not mean
serial: the MCP stdio transport multiplexes by JSON-RPC id and common SDK
servers process calls through a small worker pool, so 5 mirrors typical
upstream capacity. Drop to 1 only for a server you know is single-threaded or
backed by a fragile store.
Shedding. A shed call is reported as a readable, retry-friendly error rather
than a dropped connection: MCP tool calls get an error tool result, the REST
tool-call endpoint returns 429 with Retry-After, and the activity log
records the call with a rejected status carrying the reason (queue_full or
queue_timeout) and scope (server or global).
Batched sandbox calls. A call_tools() batch from
code execution goes through the same admission path, so these
limits are never bypassed by batching. A server capped at
max_concurrent_requests: 1 with queue_size: 9 serializes a 10-element batch;
the same server with no queue_size returns one result and nine per-slot
queue_full errors. Give servers you fan out against enough queue_size
headroom (or keep code_execution_max_parallel at their cap).
Hot reload. All limits are hot-reloadable — edit the config file and the new values govern subsequent admissions without a restart. Running calls are never interrupted, but they keep counting against the new caps: after lowering a cap, nothing new is admitted until occupancy drains below it. Raising a cap admits waiting calls immediately; queued calls keep their original deadline.
Validation. Negative values are rejected, as is a positive queue_size in a
scope whose max_concurrent_requests resolves to disabled (a queue in front of
no limiter can never admit anything). Errors name the offending scope and field.
The exception is the documented opt-out above: an explicit per-server
max_concurrent_requests: 0 is valid even when the default set defines a queue.
Only the global aggregate scope has environment overrides
(MCPPROXY_MAX_CONCURRENT_REQUESTS, MCPPROXY_QUEUE_SIZE,
MCPPROXY_QUEUE_TIMEOUT); the default set and per-server overrides are
file/API-configured.
Per-server limits over REST. POST /api/v1/servers and
PATCH /api/v1/servers/{name} accept max_concurrent_requests, queue_size
and queue_timeout alongside the other per-server fields, and
GET /api/v1/servers echoes them back. All three keep tri-state semantics on
PATCH: omitting a key leaves the stored value alone, and an explicit 0 is the
documented opt-out — it is applied, not treated as "unset".
curl -X PATCH http://127.0.0.1:8080/api/v1/servers/fragile-db \
-H "X-API-Key: $MCPPROXY_API_KEY" -H 'Content-Type: application/json' \
-d '{"max_concurrent_requests": 1, "queue_size": 2, "queue_timeout": "10s"}'What is limited, and what is not. Limits apply to upstream tool calls from
every in-process origin — the call_tool_* variants, direct-routing mode, the
REST POST /api/v1/tools/call endpoint, sandboxed code_execution scripts and
activity replay. Local tool search, coalesced tool listings and health probes
are never throttled: they are lightweight and must not be able to queue behind a
saturated upstream. The separate-process CLI debug client is out of scope.
Observability. Saturation is visible on the Prometheus surface:
| Metric | Type | Labels | Meaning |
|---|---|---|---|
mcpproxy_tool_calls_rejected_total |
counter | server, reason, scope |
Calls shed by a limit. reason = queue_full | queue_timeout; scope = server | global. server is the call's target even for a global shed. |
mcpproxy_concurrency_active |
gauge | scope, server |
Calls currently holding a slot. |
mcpproxy_concurrency_queue_depth |
gauge | scope, server |
Calls currently waiting for a slot. |
Gauges are sampled every 10s; the counter is exact — it is incremented at the
rejection itself, not derived from the internal event stream, so a burst of
sheds cannot lose increments. The same sheds also appear in the activity log
(status: rejected) — written on the same synchronous path, and exactly one row
per shed — including the ones from code_execution and replay: the rejection is
recorded at the limiter, below the MCP dispatch layer, so no origin can bypass
it and none can double-report it.
{
"debug_search": false,
"enable_prompts": true,
"aggregate_upstream_prompts": false,
"check_server_repo": true
}| Field | Type | Default | Description |
|---|---|---|---|
debug_search |
boolean | false |
Enable debug logging for search operations |
enable_prompts |
boolean | true |
Enable mcpproxy's built-in prompts (setup / troubleshoot workflows) and advertise the MCP prompts capability. Governs only the built-ins; upstream aggregation is controlled separately by aggregate_upstream_prompts. |
aggregate_upstream_prompts |
boolean | false |
Opt-in. When true, aggregate every connected upstream server's MCP prompts into mcpproxy's own prompts/list (exposed as <server>__<prompt>). Off by default so users are safe until they deliberately enable it. Requires enable_prompts: true (the default) to have any effect. Hot-reloadable. The per-server expose_prompts override further filters which servers contribute once this is on. |
check_server_repo |
boolean | true |
Enable repository detection for MCP servers (shows install commands) |
{
"mcpServers": [
{
"name": "my-server",
"protocol": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-everything"],
"working_dir": "/path/to/project",
"env": {
"API_KEY": "secret-value"
},
"enabled": true,
"quarantined": false
}
]
}| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Unique server identifier |
protocol |
string | No | Transport protocol: stdio, http, sse, streamable-http, or auto (default: inferred from command/url) |
command |
string | Yes* | Command to execute (required for stdio protocol) |
args |
array | No | Command arguments |
url |
string | Yes* | Server URL (required for http/sse/streamable-http protocols) |
headers |
object | No | HTTP headers for HTTP-based protocols |
working_dir |
string | No | Working directory for stdio servers, or for the locally-launched child of an HTTP/SSE server (default: current directory) |
env |
object | No | Environment variables for stdio servers, or for the locally-launched child of an HTTP/SSE server |
launcher_wait_timeout |
duration | No | When command is set together with an HTTP/SSE url, how long mcpproxy waits for that URL to become reachable after spawning the child (e.g. "15s", default "30s") |
health_check_interval |
duration | No | Per-server override for the global health_check_interval. "0s" disables the liveness probe for this server only. Range: 5s–1h. Omit to inherit the global value. |
tool_discovery_interval |
duration | No | Per-server override for the global tool_discovery_interval. Overrides the global/default cadence for this server only; "0s" disables the periodic tool-discovery sweep for this server (connect-time and reactive list_changed discovery still run). Range: 30s–24h. Omit to inherit the global value. |
init_timeout |
duration | No | Per-server override for the global init_timeout — the MCP initialize handshake deadline. Raise it for an upstream that warms up (caches/indexes data) before responding to initialize (e.g. "120s", "3m"); without it such a server is killed mid-startup and, with docker run --rm, retries forever. Range: 1s–30m. Omit to inherit the global value (30s default). Settable via the upstream_servers tool and mcpproxy upstream patch --init-timeout. |
max_concurrent_requests |
integer | No | Per-server cap on concurrently running upstream tool calls (see Concurrency Limits & Request Queueing). Omit to inherit server_concurrency_defaults; 0 opts this server out of per-server limiting; positive = that cap. The global limiter still applies on top. |
queue_size |
integer | No | How many calls may wait for this server's slot. Omit to inherit the default set; 0 = no pending capacity (shed immediately at the cap). |
queue_timeout |
duration | No | How long a call may wait for this server's slot (e.g. "10s"). Omit to inherit the default set (30s when a limiter is active). |
oauth |
object | No | OAuth configuration (see OAuth Configuration) |
isolation |
object | No | Per-server Docker isolation settings (see Docker Isolation) |
enabled |
boolean | No | Enable/disable server (default: true) |
quarantined |
boolean | No | Security quarantine status. The default depends on how the server arrives. A server added through the UI, CLI, REST API or an AI agent follows its trust mode at add time (quarantined under the default manual mode). A server you add by editing this file, with no quarantined key and no prior config.db record, is held for review on first load when quarantine is enabled and its trust mode is not auto (no trust_mode means manual, unless a legacy auto_approve_tool_changes: true or skip_quarantine: true resolves it to auto). An explicit "quarantined": false admits it, and an explicit true holds it. A server that is already recorded in config.db keeps its recorded state. See Servers added by hand-editing mcp_config.json for the full admission rules. |
reconnect_on_use |
boolean | No | When true, tool calls to a disconnected server trigger an immediate reconnect attempt (15s timeout) before failing (default: false) |
expose_prompts |
boolean | No | Per-server override for whether this server's MCP prompts are aggregated into mcpproxy's prompts/list. Only takes effect when the global aggregate_upstream_prompts master switch is on. Omit to expose prompts whenever the server advertises Capabilities.Prompts; false opts this server out even if it does. |
toon_output |
string | No | Per-server override for the global toon_output: off, adaptive, or always. Non-empty value wins over the global for this server's tools; omit to inherit. See TOON Output. |
forward_headers |
array | No | Names of inbound MCP client HTTP headers to forward to this server on tools/call. Exact names only. See Client Header Forwarding. Ignored for stdio and sse. [] clears it. |
created |
string | No | ISO 8601 timestamp (auto-generated) |
updated |
string | No | ISO 8601 timestamp (auto-updated) |
MCPProxy can forward selected HTTP headers from the MCP client's request to an upstream server, for example X-Tenant-Id or a per-user X-User-Id, so an upstream that keys behaviour on a header can see it. It is enabled by default but inert until a server lists header names.
{
"forward_client_headers": true,
"mcpServers": [
{
"name": "tenant-api",
"url": "https://api.example.com/mcp",
"protocol": "streamable-http",
"headers": { "Authorization": "Bearer static-token" },
"forward_headers": ["X-Tenant-Id", "X-User-Id"]
}
]
}- Format:
forward_headersis a list of header names (case-insensitive, exact match, no wildcards, at most 32 per server). Values are never configured; they come from each inbound request. - Default and switches: the top-level
forward_client_headersdefaults to on when absent. Set it tofalse, or start withMCPPROXY_FORWARD_CLIENT_HEADERS=false|0|off, to stop all forwarding without editing servers. A server with noforward_headersforwards nothing. Changes apply to the next call, with no reconnect. - Precedence: forwarded headers have the lowest precedence. They never replace a header that MCPProxy, OAuth or the server's static
headersset. A name that equals a static header key is rejected on write and skipped at runtime, so the configured value always wins. On OAuth servers the upstream always receives MCPProxy's own token. - Deny list: these are never forwarded, even if listed:
Authorization,Proxy-Authorization,X-Api-Key,Cookie,Set-Cookie,Forwarded,X-Real-Ip,X-Forwarded-*;Host,Connectionand other hop-by-hop headers (and any name in the request's ownConnectionheader),Proxy-*;Content-*,Accept,Accept-Encoding,Range,If-*,Last-Event-Id,Mcp-*;Traceparent,Tracestate,Baggage,X-Request-Id,X-Mcpproxy-*,Sec-*;User-Agent,Origin,Referer. The filter is re-applied on every request; write-time validation is only a convenience. - Transports: streamable HTTP only (
protocol: http,streamable-http, orautoresolving to it). Header forwarding is not implemented for the deprecatedssetransport (SSE upstreams still work, they just never receive forwarded headers), andstdioservers have no HTTP request to carry headers. An allowlist on either is ignored, and a warning names the header. - Which upstream requests carry headers: only the
tools/callrequest made for a call that arrives on an MCP endpoint (/mcp,/mcp/alland the other MCP mounts):call_tool_read|write|destructive, directserver__toolcalls, andcode_executionsub-calls. These never carry them:initialize,tools/list, prompts, pings and notifications, reconnects (includingreconnect_on_use), health checks, background discovery and indexing,upstream_servers refresh, quarantine inspection, the REST API (/api/v1/tools/calland friends), the CLI, and OAuth flows. - Limits: each value at most 4 KiB, 16 KiB total per request. Oversized values, empty values and values with control characters are dropped. Repeated inbound headers are joined with
,. - Redirects: if an upstream redirects a
tools/callto a different origin, the forwarded headers are removed from the redirected request. - Redaction: MCPProxy never writes forwarded values to logs, the activity log, tool-call records, audit lines, events or error text; only names appear. Trace logging masks them too. If an upstream echoes a value back in an error or a result, the copy MCPProxy stores or logs has it replaced by
[forwarded:<Name>]. This is best effort: it catches the exact value (and its JSON-escaped form), not a transformed one (base64, URL-encoding, hashing, splitting), and values shorter than 4 characters are only caught in name-anchored forms. The client still receives the unmodified result. Results that came from a call with forwarded headers are only served fromread_cacheto a request carrying the same forwarded values. - Trust warning: forwarded values are unverified client assertions, not authenticated identity. Any client that can reach
/mcpcan send any value, including another user'sX-User-Id. MCPProxy copies values and does not check them. Only forward a header to an upstream that authenticates it some other way, or when every client that can reach MCPProxy is trusted to assert it. - Limitations: MCPProxy keeps one shared upstream session per server, so
initializenever carries client headers and session-bound identity is not supported. Sampling, elicitation and the session-close request carry none. Plainhttp://to a non-loopback upstream sends values in cleartext.
To verify locally, run mcpfixture --transport http --port 18080 --echo-headers and call its echo_headers tool through MCPProxy; it returns the headers the upstream received.
stdio - Standard input/output (local processes):
{
"name": "local-server",
"protocol": "stdio",
"command": "python",
"args": ["-m", "my_mcp_server"],
"working_dir": "/path/to/project"
}http - HTTP transport:
{
"name": "remote-server",
"protocol": "http",
"url": "https://api.example.com/mcp",
"headers": {
"Authorization": "Bearer token"
}
}sse - Server-Sent Events:
{
"name": "sse-server",
"protocol": "sse",
"url": "https://api.example.com/mcp/sse"
}streamable-http - Streamable HTTP (MCP standard):
{
"name": "streamable-server",
"protocol": "streamable-http",
"url": "https://api.example.com/mcp"
}auto - Auto-detect from command or url:
{
"name": "auto-server",
"protocol": "auto",
"command": "npx",
"args": ["-y", "my-server"]
}By default command is only used for stdio servers. When you set command
together with an HTTP/SSE url and an explicit protocol of http, sse,
or streamable-http, mcpproxy will:
- Spawn the command (with
args,env,working_dir, and Docker isolation exactly like a stdio server). - Wait up to
launcher_wait_timeout(default 30s) forurlto accept a TCP connection. - Connect via the configured HTTP/SSE transport.
- Own the child's lifecycle — the process is stopped (
SIGTERM, thenSIGKILLafter a grace period) on disconnect, restart, server-disable, or mcpproxy shutdown. Unexpected exits trigger an automatic disconnect, which the existing reconnect path picks up.
{
"name": "local-http-mcp",
"protocol": "http",
"url": "http://127.0.0.1:9999/mcp",
"command": "node",
"args": ["./examples/echo-http-server.js", "--port", "9999"],
"working_dir": "/path/to/repo",
"launcher_wait_timeout": "15s",
"enabled": true
}stdout and stderr of the child are routed to the per-server log, so
mcpproxy upstream logs <name> continues to work the same way it does for
stdio servers.
protocol |
command |
url |
Behaviour |
|---|---|---|---|
stdio (explicit) |
set | any | Stdio transport, child via stdin/stdout — url ignored. |
http / sse / streamable-http (explicit) |
set | set | Locally-launched HTTP/SSE — spawn child, wait for URL, connect via network. |
http / sse / streamable-http (explicit) |
unset | set | Connect to remote URL — no spawn. |
auto or unset |
set | any | Stdio (command wins over url for back-compat — set protocol explicitly to opt into the launcher). |
auto or unset |
unset | set | HTTP/SSE remote — no spawn. |
The "command wins" rule under auto is intentional: it preserves backwards
compatibility with configurations written before the launcher feature
existed. To launch a local HTTP/SSE server you must set protocol
explicitly to one of http, sse, or streamable-http.
{
"oauth": {
"client_id": "your-client-id",
"client_secret": "secret-reference",
"redirect_uri": "http://127.0.0.1:54108/oauth/callback",
"scopes": ["repo", "user"],
"pkce_enabled": true
}
}| Field | Type | Required | Description |
|---|---|---|---|
client_id |
string | No | OAuth client ID (uses Dynamic Client Registration if empty) |
client_secret |
string | No | OAuth client secret (can reference secure storage) |
redirect_uri |
string | No | Pins the loopback callback URL (auto-allocated if not provided) |
scopes |
array | No | OAuth scopes to request |
pkce_enabled |
boolean | No | PKCE is always enabled for security; this flag is currently ignored |
An oauth block — even an empty {} — tells mcpproxy the upstream needs OAuth.
Without one, mcpproxy probes the server anonymously first and only falls back to
OAuth when that probe is rejected. Most OAuth servers reject the anonymous
initialize, so autodetection works for them.
Some servers authorise per method instead: they answer initialize and
tools/list anonymously and reject only tools/call. Google's Gmail MCP endpoint
(https://gmailmcp.googleapis.com/mcp/v1) is one. For these the anonymous probe
succeeds, so autodetection can never reach OAuth — every tool call then fails
with authorization required while the server shows as connected. Declare
OAuth explicitly:
{
"name": "Gmail",
"url": "https://gmailmcp.googleapis.com/mcp/v1",
"protocol": "http",
"oauth": {
"client_id": "<client-id>.apps.googleusercontent.com",
"client_secret": "${keyring:gmail_client_secret}",
"scopes": ["https://mail.google.com/"]
}
}With an oauth block mcpproxy skips the anonymous probe: the stored token is
attached to every request (including tools/call), and mcpproxy auth login
always starts a fresh sign-in — even when a valid token is already stored and
even though the anonymous handshake would have succeeded.
Static headers ride along with the bearer; the token store owns the
Authorization header, so a static Authorization value is not used while an
oauth block is present.
Do not add an oauth block to a server that needs no sign-in: it will wait for
a login instead of connecting anonymously (the server's health detail says so:
"the server's oauth block declares OAuth, so the anonymous probe was skipped").
Because no request is sent until a token exists, an unreachable URL on such a
server also shows as "Sign-in required" rather than a connection error until
the first login.
By default mcpproxy asks the OS for a free loopback port on the first OAuth login, then persists that port and tries to reuse it. If the saved port is taken next time, a different one is allocated — so the callback URL is not guaranteed to stay put. Some providers require the port to match the registered callback URL exactly, so it must not move at all. GitHub OAuth Apps are the common case: even with GitHub's wildcard matching enabled, that matching covers subdomains and subdirectory paths only — the host and port must still match exactly.
Set redirect_uri to pin it. mcpproxy then binds that exact port and sends that
exact string to the provider as redirect_uri:
{
"oauth": {
"client_id": "Iv1.abc123",
"redirect_uri": "http://127.0.0.1:54108/oauth/callback"
}
}The value must be an RFC 8252 loopback redirect: http scheme, a loopback host
(127.0.0.1, localhost or ::1), and an explicit port. mcpproxy binds its
callback listener to whatever path the URI specifies, so a provider that only
lets you register a different one (some publish a single shared OAuth
application whose callback path an individual user cannot change) still
works — e.g. http://127.0.0.1:54108/callback. A pin with no path at all
(http://127.0.0.1:54108) binds /, not /oauth/callback; /oauth/callback
is only the default when redirect_uri is omitted entirely and mcpproxy
allocates a dynamic port. Register the same URL with the provider.
Prefer 127.0.0.1. localhost is accepted, and the string is sent to the
provider exactly as written, but the listener binds 127.0.0.1 — on a host
whose browser resolves localhost to ::1 without falling back, the redirect
would reach a closed port. Write http://[::1]:PORT/oauth/callback if you want
the IPv6 loopback; mcpproxy then binds ::1 itself.
A malformed value, or a pinned port that is already in use, fails the login with an explicit error rather than silently falling back to a random port. The error appears in three places:
- the connection error and the server's
health.detail(it namesoauth.redirect_uriand the reason), mcpproxy upstream logs <name>(the per-server log), and- the main log (
~/Library/Logs/mcpproxy/main.logon macOS,~/.mcpproxy/logs/main.logon Linux), under theoauthlogger name.
A malformed value is also rejected up front by the write surfaces — the REST
config API (POST /api/v1/config/validate, POST /api/v1/config/apply,
PATCH /api/v1/config), the Web UI that calls them, and the MCP
upstream_servers tool. Hand-editing mcp_config.json bypasses that check by
design: a bad value already on disk must not stop the daemon from booting, so it
is reported at connect time instead.
When redirect_uri is not set, mcpproxy persists the port used by the first
successful login and reuses it for subsequent logins.
If the server previously logged in without a pin, its client registration was
issued through Dynamic Client Registration against that old port. Adding a
redirect_uri with a different port clears that registration automatically so
the next login re-registers against the pinned URL; a statically configured
client_id is never cleared.
See OAuth Documentation for complete details.
A profile is a named view over your upstream servers with a tool policy. The full model, the resolution order and the refusal texts are in Profiles; this is the configuration reference.
{
"require_mcp_auth": true,
"anonymous_profile": "",
"profiles": [
{
"name": "work-readonly",
"title": "Work · Read-only",
"description": "GitHub and Notion, read tools only",
"servers": ["github", "notion"],
"max_tier": "read",
"unannotated": "deny",
"tools": {
"allow": ["notion:update_page"],
"deny": ["github:*secret*"],
"classify": { "github:search_code": "read" }
},
"code_execution": false,
"management_tools": false,
"switchable_to": ["work-full"]
},
{ "name": "work-full", "servers": ["github", "notion", "filesystem"] }
]
}| Option | Type | Default | Description |
|---|---|---|---|
profiles[].name |
string | required | Slug ^[a-z0-9][a-z0-9_-]{0,62}$. Reserved: all, code, call, p (URL segments), active, try (REST routes). Duplicates are a fatal error |
profiles[].servers |
string[] | required | Servers the profile reaches. An unknown server is a warning and is skipped; an empty list denies everything |
profiles[].title |
string | name | Display title, at most 80 characters |
profiles[].description |
string | none | At most 500 characters |
profiles[].max_tier |
read | write | destructive |
no cap | The highest tool tier the profile admits |
profiles[].unannotated |
deny | as_write | as_read |
deny under a read or write cap, otherwise as_read |
How to treat a tool that declares no tier |
profiles[].tools.allow |
string[] | none | server:tool patterns (* is the only wildcard) admitted even above the cap. Cannot add a server |
profiles[].tools.deny |
string[] | none | Patterns hidden from the profile. Deny beats allow |
profiles[].tools.classify |
object | none | server:tool to read, write or destructive; applies only to tools with no annotations |
profiles[].code_execution |
boolean | off under a read or write cap, otherwise inherits enable_code_execution |
false removes the code_execution tool for the profile; the global flag always wins |
profiles[].management_tools |
boolean | inherit | true shows upstream_servers and quarantine_security (still limited by the caller's own permissions); false hides them |
profiles[].switchable_to |
string[] | unset (none) | Profiles a client bound to this profile, or a confined anonymous caller, may switch to with set_profile |
anonymous_profile |
string | empty (unconfined) | Confines every caller that presents no credential, or an unrecognised token while require_mcp_auth is off, to this profile. A missing profile denies everything and logs a warning |
A profile that sets only name and servers behaves exactly as before: no cap, unannotated tools count as read, no rules. Invalid input (an unknown tier or unannotated value, a malformed pattern, switchable_to naming the profile itself) is refused with the same message on every surface. Both profiles and anonymous_profile are live: an edit takes effect without a restart, and PATCH /api/v1/config reports them in changed_fields.
With require_mcp_auth off, a change that would let a client bound to a profile escape it by omitting its credential is refused with 409 binding_bypassable_without_auth (see Profiles, the binding guard). Before downgrading to a pre-profiles-v3 binary, turn require_mcp_auth on; the older binary does not know client credentials or anonymous_profile.
{
"api_key": "your-secret-api-key",
"trusted_hosts": ["mcp.example.com"],
"trusted_proxies": ["127.0.0.1"],
"read_only_mode": false,
"disable_management": false,
"allow_server_add": true,
"allow_server_remove": true
}| Field | Type | Default | Description |
|---|---|---|---|
api_key |
string | Auto-generated | API key for REST API authentication. Required; if empty, one is auto-generated, enforced, and written back to this config file (printed to the terminal once on first run; never written to the log files) |
trusted_hosts |
string[] | [] |
Non-loopback Host header values accepted on loopback listeners (reverse-proxy deployments). See below |
trusted_proxies |
string[] | [] (trust nobody) |
CIDRs or IP addresses whose X-Forwarded-For / X-Real-IP / X-Forwarded-Proto / X-Forwarded-Host headers are honoured; any other peer's forwarded headers are ignored and RemoteAddr is used. Env MCPPROXY_TRUSTED_PROXIES. Hot-reloadable. Invalid entry: trusted_proxies[N] "value" is not a valid CIDR or IP address (boot, PATCH and apply). See Reverse Proxy Deployment |
read_only_mode |
boolean | false |
Prevent all configuration modifications |
disable_management |
boolean | false |
Disable server management operations (restart, enable, disable) |
allow_server_add |
boolean | true |
Allow adding new servers via API/tools |
allow_server_remove |
boolean | true |
Allow removing servers via API/tools |
Security Notes:
- API Key: Set via
--api-keyflag,MCPPROXY_API_KEYenvironment variable, or config file - Empty API Key: Empty values are replaced with an auto-generated key; authentication is always enforced
- Auto-Generation: If no API key is provided, one is generated, persisted to the config file, and printed once to the terminal (stderr). It is deliberately not written to the log files - to recover it later, read
api_keyfrom~/.mcpproxy/mcp_config.json - Tray Integration: Tray app automatically manages API keys for core communication
One JSONL line per authorization decision and tool call, edition-neutral. Personal edition defaults to off; the server edition defaults to on (stdout, unless the native stdio transport is in use — see below). See Audit Log.
{
"audit_log": {
"enabled": true,
"stdout": false,
"path": "/var/log/mcpproxy/audit.jsonl",
"max_size_mb": 50,
"max_backups": 10,
"max_age_days": 90,
"compress": true
}
}| Field | Type | Default | Description |
|---|---|---|---|
audit_log.enabled |
boolean | personal: false; server: true |
Turn audit logging on. Restart-pinned — the sink is bound at construction |
audit_log.stdout |
boolean | server: true when the block is absent |
Write lines to stdout. Env MCPPROXY_AUDIT_LOG_STDOUT. Not used under the native stdio transport (stdout carries JSON-RPC); an explicit stdout: true with no path there fails boot with exit code 4 |
audit_log.path |
string | "" |
File to append lines to (rotated). Env MCPPROXY_AUDIT_LOG_PATH. An unwritable path fails boot with exit code 4 |
audit_log.max_size_mb |
int | 50 |
Rotate after this size. Must be positive when a path is set |
audit_log.max_backups |
int | 10 |
Rotated files to keep. Must be positive when a path is set |
audit_log.max_age_days |
int | 90 |
Delete rotated files after this many days. Must be positive when a path is set |
audit_log.compress |
boolean | true |
gzip rotated files |
When mcpproxy listens on a loopback address (the default 127.0.0.1:8080), DNS-rebinding
protection rejects any request whose Host header is not itself a loopback address with
403 Forbidden: invalid Host header. This blocks malicious websites from rebinding their
domain to 127.0.0.1 and driving a victim's browser into the local MCP server — but it
also blocks legitimate reverse proxies (nginx, Caddy, CloudPanel) that forward the public
domain in the Host header.
Add the public domain(s) to trusted_hosts to allow them:
{
"listen": "127.0.0.1:8004",
"trusted_hosts": ["mcp.example.com"]
}- Entries are hostnames, matched case-insensitively. An entry without a port matches that
host on any port; an entry with a port (
"mcp.example.com:8443") requires an exact port match. - A leading dot makes an entry a subdomain wildcard:
".example.com"matchesexample.comand every subdomain of it (Django/Vite convention). - The single entry
"*"disables Host and Origin validation entirely. Not recommended — it re-opens DNS-rebinding: any website the local user visits could drive requests into the proxy. - A request that carries an
Originheader must likewise have a loopback or trusted origin host (MCP spec requirement); requests withoutOrigin(non-browser clients, reverse proxies) are never rejected by the Origin check. - The same allowlist drives CORS on the REST API (
/api/v1/*) and the/eventsSSE stream: the requestOriginis echoed back inAccess-Control-Allow-Originonly when it is loopback or trusted, and no CORS headers are sent otherwise. Earlier versions sentAccess-Control-Allow-Origin: *there unconditionally, so a separate web app that calls the REST API cross-origin now needs its host intrusted_hosts. - Loopback hosts (
localhost,127.0.0.1,[::1]) are always accepted; requests on non-loopback listeners are never subject to Host validation. - Environment override:
MCPPROXY_TRUSTED_HOSTS(comma-separated list). - Hot-reloadable: editing the config file applies without a restart.
With trusted_hosts configured, a standard nginx block works without overriding Host:
location / {
proxy_pass http://127.0.0.1:8004;
proxy_set_header Host $host;
proxy_buffering off;
}See Reverse Proxy Deployment for a full guide covering
nginx and Caddy examples, streaming/SSE buffering, and enabling require_mcp_auth when
exposing MCPProxy beyond localhost.
The deterministic, offline tpa-descriptions baseline scanner always runs and is
the sole source of the approval verdict. The heavier Docker-based scanner plugins
and published-package-source extraction live behind the opt-in security.deep_scan
block — off by default, best-effort, and unable to change the baseline verdict
(Spec 077).
{
"security": {
"scan_timeout_default": "60s",
"integrity_check_interval": "1h",
"integrity_check_on_restart": false,
"scanner_registry_url": "",
"tpa_bundle_path": "",
"auto_baseline_scan": true,
"deep_scan": {
"enabled": false,
"fetch_package_source": true,
"disable_no_new_privileges": false,
"scanners": []
}
}
}| Field | Type | Default | Description |
|---|---|---|---|
tpa_bundle_path |
string | "" (embedded) |
Filesystem path to the tpa-db scanner-bundle.json the offline TPA scanner runs. Empty uses the corpus embedded in the build. Env override: MCPPROXY_TPA_BUNDLE_PATH, which wins over this field on every path (loader, hot-reload, /api/v1/config/apply). Re-read on config hot-reload and honoured in every transport, stdio included. A bundle that fails to read/parse/version-check/compile — or that contributes zero runnable rules — is refused and the previously active corpus stays live; the reason is surfaced as signature_bundle.load_error in GET /api/v1/security/overview and in mcpproxy security overview. |
auto_baseline_scan |
boolean | true |
Kill switch for the automatic informational baseline scan. When on (the default), every newly added server gets one free in-process Pass-1 TPA scan, and once per installation a background sweep scans pre-existing enabled servers that have never been scanned (marker persisted in BBolt, so it runs exactly once and never delays startup). The result only populates the security badge / scan summary: it never quarantines, approves, or otherwise gates a server. Disabled servers are skipped, and so are trust_mode: "scan" servers — that mode's own admission gate scans them and auto-approves on a clean verdict, so the informational path stays out of it entirely rather than risk feeding that gate; this flag does not affect that separate path. Set to false to suppress all automatic scans; manual scans keep working. Hot-reloadable — the flag is read live at each decision point. Env override: MCPPROXY_AUTO_BASELINE_SCAN (true/1/false/0), which wins over this field on every path. |
deep_scan.enabled |
boolean | false |
Master opt-in for the heavy layer. When false, no Docker scanner runs and no source extraction is attempted — only the in-process baseline scanner executes. |
deep_scan.fetch_package_source |
boolean | true (when deep scan is on) |
Whether the scanner fetches (never executes) the published source of npx/uvx package-runner servers when no local source is available. Set false for air-gapped deployments. |
deep_scan.disable_no_new_privileges |
boolean | false |
Omits --security-opt no-new-privileges from scanner container runs (snap-docker/AppArmor escape hatch). |
deep_scan.scanners |
string[] | [] |
Optional allow-list of deep scanner ids. Empty ⇒ all enabled deep scanners are eligible. |
Deprecated-key migration. The old top-level security.scanner_fetch_package_source
and security.scanner_disable_no_new_privileges keys still parse and are migrated
into security.deep_scan.* on load. The former security.auto_scan_quarantined
key was removed; a config still carrying it loads without error and the key is
ignored.
See Security Scanner Plugins for the full scanner configuration reference.
The tokenizer provides local token counting using the tiktoken library. It does not access LLMs or make API calls—it's purely for counting tokens in text locally.
{
"tokenizer": {
"enabled": true,
"default_model": "gpt-4",
"encoding": "cl100k_base"
}
}| Field | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Enable/disable token counting |
default_model |
string | "gpt-4" |
Default model name for tokenization (used to determine encoding when model not specified) |
encoding |
string | "cl100k_base" |
Default tiktoken encoding to use |
Important: The tokenizer does not access LLMs. It performs local token counting using the tiktoken algorithm:
- Local Processing: All token counting happens locally using the tiktoken library
- No Network Calls: No API requests or external services are used
- Model Mapping: The
default_modelfield is used to look up the appropriate encoding viaGetEncodingForModel() - Encoding Selection: If a model isn't recognized, it falls back to the
encodingfield orcl100k_base
The tokenizer automatically maps model names to encodings:
GPT-4o Series (uses o200k_base):
gpt-4o,gpt-4o-mini,gpt-4.1,gpt-4.5,gpt-4o-2024-05-13,gpt-4o-2024-08-06
GPT-4 & GPT-3.5 Series (uses cl100k_base):
gpt-4,gpt-4-turbo,gpt-3.5-turbo,gpt-3.5-turbo-16k,text-embedding-ada-002, etc.
Claude Models (uses cl100k_base as approximation):
claude-3-5-sonnet,claude-3-opus,claude-3-sonnet,claude-3-haiku,claude-2.1,claude-2.0,claude-instant- Note: Claude models use
cl100k_baseas an approximation. For accurate counts, use Anthropic'scount_tokensAPI.
Codex Series (uses p50k_base):
code-davinci-002,code-davinci-001,code-cushman-002,code-cushman-001
Older GPT-3 Series (uses r50k_base):
text-davinci-003,text-davinci-002,davinci,curie,babbage,ada
| Encoding | Models | Description |
|---|---|---|
o200k_base |
GPT-4o, GPT-4.5 | Latest OpenAI models |
cl100k_base |
GPT-4, GPT-3.5, Claude (approx) | Most common encoding |
p50k_base |
Codex | Code generation models |
r50k_base |
GPT-3 | Legacy models |
For GPT-4:
{
"tokenizer": {
"enabled": true,
"default_model": "gpt-4",
"encoding": "cl100k_base"
}
}For Claude Models:
{
"tokenizer": {
"enabled": true,
"default_model": "claude-3-5-sonnet",
"encoding": "cl100k_base"
}
}For GPT-4o:
{
"tokenizer": {
"enabled": true,
"default_model": "gpt-4o",
"encoding": "o200k_base"
}
}Disable Token Counting:
{
"tokenizer": {
"enabled": false
}
}- Token Usage Tracking: Counts tokens in MCP tool calls and responses
- Token Savings Calculation: Calculates token savings from caching
- Metrics & Monitoring: Provides token metrics for observability
- Response Truncation: Helps determine when to truncate large responses
{
"tls": {
"enabled": false,
"require_client_cert": false,
"certs_dir": "",
"hsts": true
}
}| Field | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | false |
Enable HTTPS/TLS |
require_client_cert |
boolean | false |
Enable mutual TLS (mTLS) for client authentication |
certs_dir |
string | "" |
Custom certificate directory (defaults to ${data_dir}/certs) |
hsts |
boolean | true |
Enable HTTP Strict Transport Security headers |
Quick Setup:
- Trust certificate:
mcpproxy trust-cert - Enable TLS: Set
"enabled": trueorMCPPROXY_TLS_ENABLED=true - Update client URLs to use
https://
See Setup Guide - HTTPS for complete details.
{
"logging": {
"level": "info",
"enable_file": false,
"enable_console": true,
"filename": "main.log",
"log_dir": "",
"max_size": 10,
"max_backups": 5,
"max_age": 30,
"compress": true,
"json_format": false
}
}| Field | Type | Default | Description |
|---|---|---|---|
level |
string | "info" |
Log level: trace, debug, info, warn, error |
enable_file |
boolean | false |
Enable file logging |
enable_console |
boolean | true |
Enable console logging |
filename |
string | "main.log" |
Log filename |
log_dir |
string | "" |
Custom log directory (defaults to OS log root; see below) |
max_size |
integer | 10 |
Maximum log file size in MB before rotation |
max_backups |
integer | 5 |
Number of backup log files to keep |
max_age |
integer | 30 |
Maximum age of log files in days |
compress |
boolean | true |
Compress rotated log files |
json_format |
boolean | false |
Use JSON format (useful for log aggregation) |
Log Locations (defaults):
- macOS:
~/Library/Logs/mcpproxy/main.log - Linux:
~/.local/state/mcpproxy/logs/main.log(or/var/log/mcpproxywhen running as root) - Windows:
%LOCALAPPDATA%\mcpproxy\logs\main.log - Per-server logs: same directory,
server-{name}.log(characters in the server name that aren't letters, digits,.,-, or_— such as the/in registry names likeio.github.evidai/polymarket-guard— are sanitized to_, so the log is always a single flat file) - Custom: set
log_dirto override (supports~expansion)
Behavior notes:
mcpproxy serveenables file logging by default unless--log-to-fileis explicitly set tofalse
See Logging Documentation for complete details.
{
"docker_isolation": {
"enabled": false,
"default_images": {
"python": "ghcr.io/astral-sh/uv:python3.13-bookworm-slim",
"uvx": "ghcr.io/astral-sh/uv:python3.13-bookworm-slim",
"node": "node:22",
"npx": "node:22"
},
"registry": "docker.io",
"network_mode": "bridge",
"memory_limit": "512m",
"cpu_limit": "1.0",
"timeout": "30s",
"extra_args": [],
"log_driver": "",
"log_max_size": "100m",
"log_max_files": "3"
}
}| Field | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | false |
Enable Docker isolation globally |
default_images |
object | See below | Map of runtime type to Docker image, merged over the built-in map (a partial map only overrides the keys it lists). The optional uvx-git key — not part of the built-in map — overrides the git-capable image used instead of the Python default when a Python package runner installs from a git+ URL (the slim uv image has no git); set it to "" to opt out. Setting it always wins, including at the value MCPProxy ships. Leave it unset and MCPProxy picks the image: your own uvx/python image if you retargeted it at your registry, otherwise ghcr.io/astral-sh/uv:python3.13-bookworm — qualified with registry when you set one |
registry |
string | "docker.io" |
Docker registry to use |
network_mode |
string | "bridge" |
Docker network mode |
memory_limit |
string | "512m" |
Memory limit for containers |
cpu_limit |
string | "1.0" |
CPU limit (1 core) |
timeout |
string | "30s" |
Container startup timeout |
extra_args |
array | [] |
Additional docker run arguments |
log_driver |
string | "" |
Docker log driver (empty = system default) |
log_max_size |
string | "100m" |
Maximum log file size |
log_max_files |
string | "3" |
Maximum number of log files |
{
"python": "python:3.11",
"python3": "python:3.11",
"uvx": "python:3.11",
"pip": "python:3.11",
"pipx": "python:3.11",
"node": "node:20",
"npm": "node:20",
"npx": "node:20",
"yarn": "node:20",
"go": "golang:1.21-alpine",
"cargo": "rust:1.75-slim",
"rustc": "rust:1.75-slim",
"binary": "alpine:3.18",
"sh": "alpine:3.18",
"bash": "alpine:3.18",
"ruby": "ruby:3.2-alpine",
"gem": "ruby:3.2-alpine",
"php": "php:8.2-cli-alpine",
"composer": "php:8.2-cli-alpine"
}{
"mcpServers": [
{
"name": "isolated-server",
"isolation": {
"enabled": true,
"image": "custom-image:latest",
"network_mode": "none",
"extra_args": ["--cap-drop=ALL"],
"working_dir": "/app",
"log_driver": "json-file",
"log_max_size": "50m",
"log_max_files": "2"
}
}
]
}| Field | Type | Description |
|---|---|---|
enabled |
boolean | Enable Docker isolation for this server (overrides global setting) |
image |
string | Custom Docker image (overrides default) |
network_mode |
string | Custom network mode for this server |
extra_args |
array | Additional docker run arguments |
working_dir |
string | Working directory inside container |
log_driver |
string | Log driver override |
log_max_size |
string | Log file size override |
log_max_files |
string | Log file count override |
See Docker Isolation Documentation for complete details.
{
"docker_recovery": {
"enabled": true,
"check_intervals": ["2s", "5s", "10s", "30s", "60s"],
"max_retries": 0,
"notify_on_start": true,
"notify_on_success": true,
"notify_on_failure": true,
"notify_on_retry": false,
"persistent_state": true
}
}| Field | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Enable Docker recovery monitoring |
check_intervals |
array | ["2s", "5s", "10s", "30s", "60s"] |
Exponential backoff intervals for health checks |
max_retries |
integer | 0 |
Maximum retry attempts (0 = unlimited) |
notify_on_start |
boolean | true |
Show notification when recovery starts |
notify_on_success |
boolean | true |
Show notification on successful recovery |
notify_on_failure |
boolean | true |
Show notification on recovery failure |
notify_on_retry |
boolean | false |
Show notification on each retry |
persistent_state |
boolean | true |
Save recovery state across restarts |
See Docker Recovery Documentation for complete details.
{
"environment": {
"inherit_system_safe": true,
"allowed_system_vars": [
"PATH",
"HOME",
"TMPDIR",
"NODE_PATH"
],
"custom_vars": {
"CUSTOM_VAR": "value"
},
"enhance_path": false
}
}| Field | Type | Default | Description |
|---|---|---|---|
inherit_system_safe |
boolean | true |
Inherit safe system environment variables |
allowed_system_vars |
array | See below | List of system variables to allow |
custom_vars |
object | {} |
Custom environment variables to set |
enhance_path |
boolean | false |
Enable PATH enhancement for Launchd scenarios |
Default Allowed System Variables:
- Core:
PATH,HOME,TMPDIR,TEMP,TMP,SHELL,TERM,LANG,USER,USERNAME - Windows-specific:
USERPROFILE,APPDATA,LOCALAPPDATA,PROGRAMFILES,SYSTEMROOT,COMSPEC - Unix/XDG:
XDG_CONFIG_HOME,XDG_DATA_HOME,XDG_CACHE_HOME,XDG_RUNTIME_DIR - Locale: all
LC_*variables (e.g.,LC_ALL,LC_CTYPE, …) - Custom additions:
custom_varsmerged on top
Proxy variables are never inherited by default.
HTTP_PROXY,HTTPS_PROXY,NO_PROXY,ALL_PROXY, andFTP_PROXYare deliberately excluded from the default allow-list because proxy URLs commonly embed credentials (http://user:pass@proxy). To forward them to upstream stdio servers, opt in withforward_proxy_env(see below).
{
"forward_proxy_env": true
}| Field | Type | Default | Description |
|---|---|---|---|
forward_proxy_env |
boolean | false |
Forward ambient proxy environment variables to spawned stdio upstream servers, with credentials redacted |
When forward_proxy_env is true, mcpproxy forwards the proxy variables present
in its own environment (HTTP_PROXY/http_proxy, HTTPS_PROXY/https_proxy,
NO_PROXY/no_proxy, ALL_PROXY/all_proxy, FTP_PROXY/ftp_proxy — both
spellings are recognized) to each spawned stdio upstream. Any userinfo
(user:password@) is stripped from the value before forwarding, so
credentials never reach upstream servers while the proxy host/port is preserved.
An explicitly configured proxy value (via custom_vars or a server's env)
always takes precedence and suppresses forwarding of the ambient value, including
the other-cased alias.
macOS GUI/launchd note: when launched from the Dock/Launchpad or the login item, mcpproxy inherits a minimal environment that may not contain your proxy variables. In that case set the proxy explicitly under
custom_varsor a server'senvblock.
Controls how upstream MCP tools are exposed to AI agents on the default /mcp endpoint.
{
"routing_mode": "retrieve_tools"
}| Field | Type | Default | Description |
|---|---|---|---|
routing_mode |
string | "retrieve_tools" |
How tools are exposed: retrieve_tools, direct, or code_execution |
Available modes:
| Mode | Description |
|---|---|
retrieve_tools |
BM25 search via retrieve_tools + call_tool_read/write/destructive (default, most token-efficient) |
direct |
All upstream tools exposed directly as serverName__toolName |
code_execution |
JavaScript orchestration via code_execution tool with tool catalog |
All three modes are always available on dedicated endpoints regardless of config: /mcp/all (direct), /mcp/code (code_execution), /mcp/call (retrieve_tools).
See Routing Modes for complete details.
Controls only the serialization of retrieve_tools responses (Spec 085) — never the query, ranking, or result set. Orthogonal to routing_mode.
{
"tool_response_mode": "full"
}| Field | Type | Default | Description |
|---|---|---|---|
tool_response_mode |
string | "full" |
full returns complete inputSchema entries (pre-Spec-085 behavior, byte-identical). compact returns one-line signatures instead: per entry id, score, sig, first-sentence desc, and a lossy flag, plus one top-level hint line. |
- Compact signatures:
*marks a required parameter (never elided),~marks a lossy collapse (nested objects, long enums — calldescribe_toolfor the full schema), short enums/defaults are inlined (e.g.(origin*:str, ttl:int=3600)). - Per-call override: the
detailparameter onretrieve_tools(compact|full) overrides the configured mode for that call only. - describe_tool: in compact mode, agents fetch full definitions on demand with
describe_tool(batch of 1–5server:toolids; same visibility rules as search). - Hot-reload: changes apply on the next call via the config file reload or
POST /api/v1/config/apply— no restart. - Env:
MCPPROXY_TOOL_RESPONSE_MODE· Flag:--tool-response-mode· UI: Settings → General → "Detail in tool-search results" (Web UI and macOS tray).
Controls only the serialization of the direct enumeration surface (Spec 102) — /mcp/all, plus /mcp and its legacy aliases /v1/tool_code and /v1/tool-code when routing_mode is direct. It never changes which tools are listed, only how each one is rendered.
{
"direct_tool_response_mode": "full"
}| Field | Type | Default | Description |
|---|---|---|---|
direct_tool_response_mode |
string | "full" |
full lists every upstream tool with its complete inputSchema (pre-Spec-102 behavior, byte-identical); an empty value means full. deferred keeps each tool's name and annotations but advertises inputSchema as exactly {"type": "object"}, drops outputSchema, and appends a compact signature line to the description. |
- Deferred entries: description first, then the signature on its own line — e.g.
[github] Create a new issue./create_issue(owner*:str, repo*:str, title*:str, labels:[str], milestone~:obj). As in compact mode,*marks a required parameter and~marks a lossy collapse. - describe_tool: registered on the direct surface too, so agents recover a full schema on demand (batch of 1–5 ids). It accepts both id spellings the surface can hand an agent:
server:tooland the direct surface's ownserver__tool. - A wrong guess never reaches the upstream: a call whose arguments do not fit the stored schema is rejected pre-dispatch with an
invalid_paramserror embedding the full stored schema plus a hint, so the agent can correct itself without a separate lookup. (The schema is returned; whether the agent's next attempt is valid is up to the agent.) - Separate axis from
tool_response_mode: that key governsretrieve_toolsresponses, this one governs the direct enumeration surface, and setting one never moves the other.tool_response_mode: "compact"together withdirect_tool_response_mode: "deferred"is a legal, intentional combination — each still governs only its own surface. - Not a
routing_modevalue:routing_mode: "schema_deferred"is rejected by config validation with a message naming the supported composition (routing_mode: "direct"withdirect_tool_response_mode: "deferred"). - Hot-reload: flipping the mode rebuilds the direct surface and emits
notifications/tools/list_changedto connected direct-surface sessions — no restart. - Env:
MCPPROXY_DIRECT_TOOL_RESPONSE_MODE· Flag:--direct-tool-response-mode· UI: Settings → General → "Detail in Direct-mode listings" (Web UI and macOS tray).
Text returned in the MCP initialize response to guide AI agents on how to use the proxy (e.g., use retrieve_tools to discover existing tools rather than search_servers).
{
"instructions": "Use retrieve_tools to discover tools before assuming a capability is unavailable."
}| Field | Type | Default | Description |
|---|---|---|---|
instructions |
string | (built-in) | Custom instructions sent in the MCP initialize response. When empty, a built-in default explains the retrieve_tools → call_tool_* workflow and warns against using search_servers for existing tools. |
You can edit this from the Web UI under Settings → Advanced → MCP server instructions. The textarea shows the built-in default as a greyed-out placeholder; clearing it restores that default.
Note: Applied at startup / on the next client connect — editing this value does not hot-reload into already-connected MCP sessions.
Whatever the base text, each connection also gets a per-caller YOUR ACCESS block appended. It lists the active profile, the connected servers that caller can reach and its allowed operations, all filtered to its profile and agent-token scope. See Agent Instructions.
{
"advertise_upstream_servers": false
}| Field | Type | Default | Description |
|---|---|---|---|
advertise_upstream_servers |
bool | true |
Name the caller's reachable upstream servers in the initialize instructions and in the retrieve_tools description, so agents use proxied tools instead of shell CLIs. Names are always filtered to the caller's profile and agent-token scope. Set false to keep server names out of client context; operation limits are still stated. Read live. |
Warning: the text is operator-published content, returned verbatim to every client that initializes — including agent tokens scoped to a subset of servers. Do not put server names, hostnames, credentials or other secrets in it.
SHA256 hash-based tool approval system that detects changes to tool descriptions and schemas.
{
"quarantine_enabled": true
}| Field | Type | Default | Description |
|---|---|---|---|
quarantine_enabled |
boolean | true |
Enable tool-level quarantine globally |
Per-server tool-change auto-approval is configured on the server entry:
{
"mcpServers": [
{
"name": "trusted-server",
"command": "my-server",
"skip_quarantine": true
}
]
}| Field | Type | Default | Description |
|---|---|---|---|
auto_approve_tool_changes |
boolean (tri-state) | unset (= false) |
Auto-approve all post-baseline tool changes AND additions for this server (disables per-server rug-pull protection). The active per-server control. A trusted server's baseline is auto-approved regardless of this flag. |
skip_quarantine |
boolean | false |
Deprecated — superseded by auto_approve_tool_changes. A legacy skip_quarantine: true is migrated onto auto_approve_tool_changes on load only when it is unset (an explicit false overrides the legacy flag). |
See Tool Quarantine for complete details.
{
"enable_code_execution": true,
"code_execution_timeout_ms": 120000,
"code_execution_max_tool_calls": 0,
"code_execution_pool_size": 10,
"code_execution_max_parallel": 8
}| Field | Type | Default | Description |
|---|---|---|---|
enable_code_execution |
boolean | true |
Enable JavaScript/TypeScript code execution tool (on by default since v0.66.0; set false to switch it off). While false the code_execution tool is not advertised in tools/list. Hot-reloaded: a flip adds/removes the tool and sends notifications/tools/list_changed |
code_execution_timeout_ms |
integer | 120000 |
Default timeout in milliseconds (1-600000, max 10 minutes) |
code_execution_max_tool_calls |
integer | 0 |
Maximum tool calls per execution (0 = unlimited) |
code_execution_pool_size |
integer | 10 |
Number of JavaScript VM instances in pool (1-100) |
code_execution_max_parallel |
integer | 8 |
Default concurrency for call_tools() batches (1-32). Hot-reloaded; applies to executions that start after the change |
Code execution supports both JavaScript (ES2020+) and TypeScript. TypeScript code is automatically transpiled via esbuild before execution.
Inside a script, call_tool(server, tool, args) runs one upstream tool at a time and call_tools(requests, options) fans out independent calls in parallel:
var slots = call_tools([
{server: "github", tool: "get_pull_request", args: {owner: "acme", repo: "api", pullNumber: 1}},
{server: "github", tool: "get_pull_request", args: {owner: "acme", repo: "api", pullNumber: 2}}
], {max_parallel: 5});
// slots[i] is {ok: true, result} or {ok: false, error} for requests[i]requests holds at most 100 elements; each costs one unit of code_execution_max_tool_calls budget. Concurrency precedence is options.max_parallel (1-32) > code_execution_max_parallel > built-in 8.
Batching vs. per-server limits. Concurrency limits still govern each element. A server with
max_concurrent_requestsset and noqueue_sizesheds everything over the cap — a 10-element batch againstmax_concurrent_requests: 1returns 1 result and 9 per-slotqueue_fullerrors. Give such serversqueue_sizeheadroom (or lowermax_parallel) before fanning out against them.
Long workflows do not have to be re-sent inline on every call. A <name>.js / <name>.ts file placed in the scripts/ directory next to this configuration file (~/.mcpproxy/scripts/ by default, <dir-of---config>/scripts/ when --config points elsewhere) is invocable by name — {"script": "<name>", "input": {...}} over MCP/REST, or mcpproxy code exec --script <name>.
There is no configuration key for this: the directory convention is the whole surface, and the scripts directory is never derived from --data-dir. Names are 1-64 characters of A-Za-z0-9_- (never a path), files are lowercase .js/.ts up to 256 KB, and each invocation re-reads the file, so an atomic replacement takes effect on the next run with no restart. mcpproxy code scripts list (or GET /api/v1/code/scripts) lists what exists; nothing writes scripts through any API.
See Code Execution Documentation for complete details, and Stored Scripts for the authoring rules.
{
"features": {
"enable_runtime": true,
"enable_event_bus": true,
"enable_sse": true,
"enable_observability": true,
"enable_health_checks": true,
"enable_metrics": true,
"enable_tracing": false,
"enable_oauth": true,
"enable_quarantine": true,
"enable_docker_isolation": false,
"enable_search": true,
"enable_caching": true,
"enable_async_storage": true,
"enable_web_ui": true,
"enable_debug_logging": false,
"enable_contract_tests": false
}
}Note: Feature flags are typically managed internally. Most users don't need to modify these settings.
The three default registries ship built-in and require no configuration. Use
the registries array only to add your own custom source:
{
"registries": [
{
"id": "mycorp",
"name": "My Corp Registry",
"description": "Internal MCP server catalog",
"url": "https://registry.mycorp.example/",
"servers_url": "https://registry.mycorp.example/v0.1/servers",
"tags": ["internal"],
"protocol": "modelcontextprotocol/registry"
}
]
}| Field | Type | Description |
|---|---|---|
id |
string | Unique registry identifier |
name |
string | Display name |
description |
string | Registry description |
url |
string | Registry homepage |
servers_url |
string | API endpoint for server listings |
tags |
array | Registry tags (e.g., ["verified"]) |
protocol |
string | Registry protocol type |
count |
number/string | Number of servers in registry (auto-populated) |
SSRF guard (allow_private_registry_fetch). Because the daemon fetches the
URL you configure, registry fetches refuse any host that is — or resolves to — a
non-routable address (loopback, RFC1918/CGNAT private, link-local including the
169.254.169.254 cloud-metadata endpoint). This bounds CWE-918 request forgery
against internal services. Set this top-level flag to true only if you
intentionally run a trusted registry mirror on an internal/private address:
{ "allow_private_registry_fetch": true }
⚠️ The opt-out is blanket (all-or-nothing). Setting ittruelifts the guard for every non-routable range at once — loopback, RFC1918/CGNAT private, link-local and the169.254.169.254cloud-metadata endpoint. There is no way to allow only loopback: enabling it for a localhost dev registry also re-opens the cloud-metadata SSRF vector (e.g.registry add-source https://169.254.169.254/...will then succeed). Enable it only for trusted local/dev use, ideally on hosts with no cloud-metadata exposure. The flag takes effect only on daemon (re)start / config reload.
Default false (secure). See Registries Documentation.
Default Registries (shipped built-in, no configuration required):
official— Official MCP Registry (modelcontextprotocol/registry): primary, zero-config aggregatorreference— Reference Servers (builtin/reference): curated@modelcontextprotocolservers, shipped in-binary so the basics work offlinedocker-mcp-catalog— Docker MCP Catalog (custom/docker): signed-container MCP server inventory
Deprecated former-defaults: earlier versions also shipped
pulse,smithery,fleur,azure-mcp-demo, andremote-mcp-serversas defaults. These were removed and are pruned from an existingmcp_config.jsonon load, so upgrades converge to the three defaults above. Genuinely user-added custom registries are never touched;pulse/smitherycan be added back as custom sources.
See Registries Documentation and Search Servers Documentation for complete details.
Controls the usage-statistics aggregate that powers the Web UI usage graphs (spec 069). The aggregate is built incrementally from the activity log, kept in memory as an immutable snapshot, and periodically persisted so it survives restarts without a full re-scan.
{
"observability": {
"usage_cache_ttl": "5s",
"usage_persist_interval": "30s"
}
}| Field | Type | Default | Description |
|---|---|---|---|
usage_cache_ttl |
duration string | 5s |
Freshness bound for the usage endpoint's read cache on wide time windows. |
usage_persist_interval |
duration string | 30s |
How often the in-memory usage aggregate snapshot is flushed to storage (also flushed on graceful shutdown). |
Both fields are optional, accept Go duration strings (e.g. "10s", "1m"),
and are hot-reloadable. Non-positive values fall back to the defaults.
Controls the background upgrade-awareness checker (Spec 079). MCPProxy
periodically queries GitHub Releases and surfaces "update available" on
mcpproxy status / doctor, a startup log line, the Web UI (sidebar badge +
dismissible banner), and the trays. Checks never block and fail silently when
offline.
{
"update_check": {
"enabled": true,
"channel": "stable"
}
}| Field | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Master switch for update checking. When false, no network check is performed (background poll and the manual /api/v1/info?refresh=true re-check) and no upgrade nudge appears on any surface — the update object is omitted from /api/v1/info. |
channel |
string | "stable" |
Release channel: "stable" (GitHub releases/latest; prereleases never offered) or "rc" (prerelease tags such as v0.47.0-rc.1 included). Released builds ignore this field — the running binary's own version is authoritative (a stable build is never offered an RC; an RC build always tracks rc); it only applies to dev/unstamped builds. See docs/prerelease-builds.md. |
Both keys are optional and hot-reloadable: editing them (config file or
POST /api/v1/config/apply) takes effect without a restart, and re-enabling
triggers a prompt re-check.
Environment-variable precedence — the existing switches keep working and win over the config keys (operator override):
| Variable | Effect |
|---|---|
MCPPROXY_DISABLE_AUTO_UPDATE=true |
Force-disables update checking even when update_check.enabled is true. Read by the core and the macOS tray — one export silences both, including the tray's one-click updater. |
MCPPROXY_ALLOW_PRERELEASE_UPDATES=true |
Force-selects the prerelease (rc) channel even when update_check.channel is stable. |
CI=true / CI=1 |
Suppresses every update nudge and the tray's unattended checks (non-interactive context). Machine-readable fields keep reporting the facts, and a user-initiated "Check for Updates" still runs. |
Both keys and both switches are reported to the tray as an explicit contract —
update_policy in GET /api/v1/info — rather than inferred from missing data.
The macOS one-click updater, the channel matrix and the release-infrastructure
side (feed, enclosure, signing keys) are documented in
Auto-Update.
The env vars only widen in one direction (disable checks / enable
prereleases); they cannot force-enable checking that config disabled — with
update_check.enabled: false, checks stay off regardless of environment.
Check cadence and quiet environments (Spec 079 US3):
- The background check runs at most daily and backs off on failure
(each consecutive failed check doubles the wait, capped at 8× the
interval) — offline or rate-limited environments are treated as
"unknown", never retried aggressively and never surfaced as an error.
A manual
/api/v1/info?refresh=truebypasses the backoff. - With
CI=true(orCI=1, the same convention the telemetry filter uses) the process is treated as non-interactive: the startup "Update available" log line is demoted to debug and theupdatepayload carriesnudges_suppressed: true, which hides the Web UI banner. The machine-readable facts (mcpproxy status,doctor,/api/v1/info) are unaffected.
See Version Updates for where updates are surfaced.
Here's a complete configuration example with all major sections:
Note: Leaving api_key empty will cause MCPProxy to generate and enforce a new key on startup.
{
"listen": "127.0.0.1:8080",
"data_dir": "~/.mcpproxy",
"enable_socket": true,
"api_key": "",
"tools_limit": 15,
"tool_response_limit": 20000,
"call_tool_timeout": "2m",
"debug_search": false,
"enable_prompts": true,
"check_server_repo": true,
"tokenizer": {
"enabled": true,
"default_model": "gpt-4",
"encoding": "cl100k_base"
},
"tls": {
"enabled": false,
"require_client_cert": false,
"hsts": true
},
"logging": {
"level": "info",
"enable_file": false,
"enable_console": true,
"filename": "main.log",
"max_size": 10,
"max_backups": 5,
"max_age": 30,
"compress": true,
"json_format": false
},
"mcpServers": [
{
"name": "everything",
"protocol": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-everything"],
"enabled": true,
"quarantined": false
},
{
"name": "github",
"protocol": "http",
"url": "https://api.github.com/mcp",
"oauth": {
"scopes": ["repo", "user"],
"pkce_enabled": true
},
"enabled": true
}
],
"docker_isolation": {
"enabled": false
},
"docker_recovery": {
"enabled": true,
"notify_on_start": true,
"notify_on_success": true,
"notify_on_failure": true
},
"environment": {
"inherit_system_safe": true,
"allowed_system_vars": ["PATH", "HOME", "TMPDIR"],
"custom_vars": {},
"enhance_path": false
},
"enable_code_execution": true,
"code_execution_timeout_ms": 120000,
"code_execution_max_tool_calls": 0,
"code_execution_pool_size": 10,
"code_execution_max_parallel": 8,
"read_only_mode": false,
"disable_management": false,
"allow_server_add": true,
"allow_server_remove": true
}Many configuration options can be overridden via environment variables:
| Environment Variable | Config Field | Description |
|---|---|---|
MCPPROXY_LISTEN / MCPP_LISTEN |
listen |
Network binding address |
MCPPROXY_API_KEY |
api_key |
API key for authentication (empty values trigger auto-generation; auth remains enabled) |
MCPPROXY_TRUSTED_HOSTS |
trusted_hosts |
Comma-separated Host allowlist for loopback listeners behind a reverse proxy |
MCPPROXY_TRUSTED_PROXIES |
trusted_proxies |
Comma-separated CIDRs/IPs whose X-Forwarded-* headers are honoured |
MCPPROXY_AUDIT_LOG_ENABLED |
audit_log.enabled |
Turn audit logging on/off |
MCPPROXY_AUDIT_LOG_PATH |
audit_log.path |
Audit log file path |
MCPPROXY_AUDIT_LOG_STDOUT |
audit_log.stdout |
Write audit lines to stdout |
MCPPROXY_TLS_ENABLED |
tls.enabled |
Enable HTTPS/TLS |
MCPPROXY_TLS_REQUIRE_CLIENT_CERT |
tls.require_client_cert |
Enable mTLS |
MCPPROXY_CERTS_DIR |
tls.certs_dir |
Custom certificates directory |
MCPPROXY_DATA |
data_dir |
Override data directory |
MCPPROXY_TOOL_RESPONSE_MODE |
tool_response_mode |
retrieve_tools serialization: full (default) or compact |
MCPPROXY_DIRECT_TOOL_RESPONSE_MODE |
direct_tool_response_mode |
Direct enumeration surface serialization: full (default) or deferred |
MCPPROXY_MAX_CONCURRENT_REQUESTS |
max_concurrent_requests |
Global aggregate cap on concurrent upstream tool calls (0 disables it). See Concurrency Limits |
MCPPROXY_QUEUE_SIZE |
queue_size |
Global aggregate wait-queue length (0 = shed at the cap) |
MCPPROXY_QUEUE_TIMEOUT |
queue_timeout |
Global aggregate queue wait budget, e.g. 30s |
MCPPROXY_FORWARD_CLIENT_HEADERS |
forward_client_headers |
false, 0 or off disables client header forwarding for the process. Never persisted to the config file. Any other value is ignored. |
MCPPROXY_DISABLE_OAUTH |
- | Disable OAuth for testing |
HEADLESS |
- | Run in headless mode |
Prefix rules:
- General settings also accept the
MCPP_prefix (hyphens become underscores), e.g.,MCPP_TOOLS_LIMIT,MCPP_ENABLE_PROMPTS. - TLS/listen/data have additional convenience overrides with the
MCPPROXY_prefix as listed above.
Priority: Environment variables > Config file > Defaults
MCPProxy validates configuration on startup. Common validation errors:
- Invalid listen address: Must be
host:portor:portformat - Invalid tools_limit: Must be between 1 and 1000
- Missing server name: Each server must have a unique name
- Invalid protocol: Must be
stdio,http,sse,streamable-http, orauto - Missing command: stdio servers require
commandfield - Missing url: HTTP-based servers require
urlfield - Invalid forward_headers: names must be valid HTTP tokens, not on the deny list, at most 32 entries, and not equal to a key of the same server's static
headers. Writes through the REST API,upstream_serversand config apply are rejected with the header name; a config file with a bad entry still loads, the entry is dropped and a warning names the header and server. - Invalid timeout: Must be a valid duration string (e.g.,
"30s","2m")
Run mcpproxy doctor to check configuration health.
- Setup Guide - Initial setup and client configuration
- OAuth Documentation - OAuth authentication setup
- Docker Isolation - Docker security isolation
- Logging - Logging configuration and management
- Code Execution - JavaScript code execution
- Search Servers - MCP server discovery
- TOON Output - Adaptive TOON encoding of tool results