docs/configuration/upstream-servers.md
MCPProxy can connect to multiple MCP servers simultaneously, providing unified access through a single endpoint.
MCPProxy supports three types of upstream connections:
Local servers that communicate via standard input/output:
{
"name": "filesystem",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/projects"],
"protocol": "stdio",
"enabled": true
}
Remote servers accessible via HTTP/HTTPS:
{
"name": "remote-server",
"url": "https://api.example.com/mcp",
"protocol": "http",
"enabled": true
}
Servers requiring OAuth 2.1 authentication:
{
"name": "github-server",
"url": "https://api.github.com/mcp",
"protocol": "http",
"oauth": {
"client_id": "your-client-id",
"scopes": ["repo", "user"]
},
"enabled": true
}
| Option | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique identifier for the server |
command | string | For stdio | Command to execute |
args | array | No | Command arguments |
url | string | For HTTP | Server URL |
protocol | string | Yes | stdio or http |
enabled | boolean | No | Whether server is active (default: true) |
working_dir | string | No | Working directory for stdio servers |
env | object | No | Environment variables to pass |
oauth | object | No | OAuth configuration |
auth_broker | object | No | Server-edition per-user token brokering. See Auth Broker. The authorization_endpoint key is required when auth_broker.mode is oauth_connect. |
health_check_interval | duration | No | Per-server override for the liveness ping cadence (0s disables; falls back to the global value, then the 30s default). No-op for Docker-isolated servers. |
tool_discovery_interval | duration | No | Per-server override for the tools/list re-index sweep (0s disables; falls back to the global value, then the 5m default). |
See Tool Discovery & Health Check Intervals for the global defaults, accepted ranges, and trade-offs.
Both HTTP headers and stdio env are first-class config fields you can
inspect and edit from the Web UI, the macOS tray, the CLI, and the REST
API. The wire format and semantics are identical across surfaces.
The REST API (GET /api/v1/servers), the SSE servers.changed event, and
the MCP upstream_servers list tool all redact sensitive header values
by default. The mask format is:
••••<last2> (<N> chars)
…e.g. a 71-character Bearer token whose last two characters are 59
appears on the wire as ••••59 (71 chars). The mask preserves enough
context to identify which token is in use without leaking the secret.
Values that are already secret references — ${keyring:NAME} or
${env:VAR} — pass through unchanged: they're labels, not secrets.
This redaction was added in PR #425 to close a real exfiltration path —
a prompt-injected agent calling upstream_servers list would otherwise
get back another upstream's Bearer token in plaintext.
To disable redaction (for debugging only), set reveal_secret_headers: true
in mcp_config.json. It's not normally needed: the editing flow
described below works without ever exposing the plaintext to the client.
The Server Detail page → Configuration tab has dedicated Headers and Environment Variables cards with per-row affordances:
+ Add header / + Add variable button at the top.${keyring:NAME}.The Convert flow works even on masked headers: the backend has the real
value in mcp_config.json and never needs the client to send it.
${keyring:…} references render as a labeled chip instead of a masked
literal, so converted headers are visually distinct.
# Upsert one or more headers
mcpproxy upstream patch synapbus --header "X-Trace: on"
# Rotate Authorization in place
mcpproxy upstream patch synapbus --header "Authorization: Bearer new-token"
# Delete one
mcpproxy upstream patch synapbus --header-remove "X-Stale"
# Mix set + delete in a single round-trip
mcpproxy upstream patch synapbus --header "X-New: v" --header-remove "X-Old"
# Env vars (stdio servers)
mcpproxy upstream patch obsidian-pilot --env "LOG_LEVEL=debug"
mcpproxy upstream patch obsidian-pilot --env-remove "OBSOLETE"
See Management Commands for the full flag reference.
PATCH /api/v1/servers/{name} follows JSON Merge Patch
(RFC 7396):
curl -X PATCH -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"headers":{"X-New":"value","X-Stale":null}}' \
http://127.0.0.1:8080/api/v1/servers/synapbus
null value → deleteSee REST API › PATCH for the
full reference, including the
/config-to-secret
endpoint that backs the "Convert to secret" affordance.
headers and env values support two reference shapes that get resolved
at request time rather than stored in plaintext:
| Reference | Source | Lifetime |
|---|---|---|
${keyring:NAME} | OS keyring entry called NAME | Persists across restarts; managed via the Secrets page or mcpproxy secrets CLI |
${env:VAR} | The VAR environment variable on the mcpproxy process | Tied to the shell/launcher that started mcpproxy |
When mcpproxy connects to an upstream it substitutes the reference with the actual secret. The reference itself never reaches the upstream MCP server. See Keyring Integration for the full secret-storage story.
For enhanced security, stdio servers can run in Docker containers:
{
"name": "isolated-server",
"command": "npx",
"args": ["-y", "some-mcp-server"],
"protocol": "stdio",
"isolation": {
"enabled": true,
"image": "node:20",
"network_mode": "bridge"
}
}
Note: Memory and CPU limits are configured at the global level in docker_isolation, not per-server.
See Docker Isolation for complete documentation.
When MCPProxy starts, it:
mcp_config.jsonWhen MCPProxy stops, it performs graceful shutdown of all subprocesses:
Process groups: Child processes (spawned by npm/npx/uvx) are placed in a process group, ensuring all related processes are terminated together.
See Shutdown Behavior for detailed documentation.
New servers added via AI clients are automatically quarantined for security review. See Security Quarantine for details.