Skip to content

Commit 4738610

Browse files
committed
docs: cover PR #466 headers/env editing + Convert-to-secret + new endpoints
REST API (docs/api/rest-api.md): - New section on Header redaction and the ••••<last2> (<N> chars) mask format, when reveal_secret_headers applies, and why operators rarely need to flip it. - New PATCH /api/v1/servers/{name} endpoint documented with full JSON Merge Patch (RFC 7396) semantics: non-null upserts, JSON null deletes, absent keys preserved. Includes worked curl examples and the empty-string-is-not-delete gotcha. - New POST /api/v1/servers/{name}/config-to-secret endpoint — atomically moves a header/env value out of mcp_config.json into the OS keyring without the client ever holding the plaintext. CLI (docs/cli/management-commands.md): - New `upstream patch` subcommand with --header / --header-remove / --env / --env-remove flags and the deep-merge guarantee that no un-named key gets disturbed. Configuration guide (docs/configuration/upstream-servers.md): - New "Headers, Environment Variables, and Secrets" section explaining the wire mask, the three editing surfaces (Web UI / macOS tray / CLI / REST), and the ${keyring:NAME} / ${env:VAR} reference shapes. - Cross-links to the REST PATCH reference, the CLI subcommand, and the keyring integration page. Web UI (docs/web-ui/server-detail.md, new): - Dedicated page for the Server Detail Configuration tab focused on the Headers and Environment Variables cards. Documents the value formats (masked literal, keyring chip, env chip, plain), the per-row actions (add / edit / delete / convert), and the convert-to-secret flow including the atomic backend swap. - Two embedded screenshots showing the Headers card and the Convert-to-secret modal. Screenshots (docs/screenshots/server-detail/): - web-headers-card.png — the Headers card with masked Authorization + Convert to secret button. Captured against the live local instance with synapbus configured with a real Bearer token. - web-convert-modal.png — the modal preview with auto-suggested secret name and the ${keyring:NAME} live preview. Cross-references between the four pages so a reader can land anywhere and find the relevant detail in two clicks.
1 parent fc4051f commit 4738610

6 files changed

Lines changed: 437 additions & 1 deletion

File tree

docs/api/rest-api.md

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,36 @@ Get server status and statistics.
117117

118118
List all upstream servers with unified health status.
119119

120+
##### Header redaction and the mask format
121+
122+
By default, sensitive header values (`Authorization`, `X-API-Key`, `Cookie`,
123+
`Set-Cookie`, etc.) are replaced with a length-preserving mask of the form
124+
`••••<last2> (<N> chars)` before serialization. This applies to:
125+
126+
- `GET /api/v1/servers` and its single-server children
127+
- The `/events` SSE `servers.changed` payloads
128+
- The `upstream_servers list` MCP tool
129+
130+
The mask preserves enough information to identify which token is in use
131+
(the last two characters + total length) while keeping the secret out of
132+
the response. Values that are already secret references — `${keyring:NAME}`
133+
or `${env:VAR}` — pass through unchanged because they're labels, not
134+
secrets.
135+
136+
Setting `reveal_secret_headers: true` in
137+
[`mcp_config.json`](../configuration/config-file.md) disables redaction on
138+
all three channels. This is **not normally needed**: the Web UI / macOS
139+
tray / CLI can edit, delete, and convert-to-secret without ever seeing
140+
the plaintext, because the PATCH endpoint deep-merges (omitted keys are
141+
preserved) and the [`config-to-secret`](#post-apiv1serversnameconfig-to-secret)
142+
endpoint reads the real value server-side. Flip the flag only if you
143+
need to inspect a raw value through the API for debugging.
144+
145+
The MCP `upstream_servers` tool was the original motivator for redaction
146+
(see [PR #425](https://github.com/smart-mcp-proxy/mcpproxy-go/pull/425)) —
147+
a prompt-injected agent could otherwise read another upstream's PAT via
148+
`upstream_servers list`.
149+
120150
**Response:**
121151
```json
122152
{
@@ -167,6 +197,118 @@ List all upstream servers with unified health status.
167197
| `detail` | string | Optional additional context about the status |
168198
| `action` | string | Suggested remediation: `login`, `restart`, `enable`, `approve`, `view_logs`, or empty |
169199

200+
#### PATCH /api/v1/servers/{name}
201+
202+
Partial update of an existing upstream server. All request fields are optional;
203+
omitted fields are preserved as-is.
204+
205+
The map-typed fields `headers` and `env` follow **JSON Merge Patch
206+
([RFC 7396](https://www.rfc-editor.org/rfc/rfc7396))** semantics:
207+
208+
| Value in patch body | Effect on stored map |
209+
|---|---|
210+
| key present with a non-null string value | upsert (add or replace that key) |
211+
| key present with JSON `null` | delete that key |
212+
| key absent from the patch body | preserve as-is |
213+
214+
This is the same convention the MCP `upstream_servers patch` tool uses. It
215+
lets the Web UI / macOS tray / CLI send a minimal diff — keys that match
216+
the server's current masked view (`••••<last2> (<N> chars)` — see
217+
[Header redaction](#header-redaction-and-the-mask-format) below) simply stay
218+
out of the patch body, so the real stored value is never overwritten by the
219+
mask string.
220+
221+
**Request body** ([`AddServerRequest`](https://github.com/smart-mcp-proxy/mcpproxy-go/blob/main/internal/httpapi/server.go) — all fields optional):
222+
223+
```json
224+
{
225+
"url": "https://api.example.com/mcp",
226+
"command": "uvx",
227+
"args": ["mcp-server-foo"],
228+
"env": {"API_KEY": "new-value", "OLD_VAR": null},
229+
"headers": {"X-Trace": "on", "X-Stale": null},
230+
"working_dir": "/path/to/dir",
231+
"protocol": "http",
232+
"enabled": true,
233+
"quarantined": false,
234+
"isolation": {"enabled": true, "image": "node:20"}
235+
}
236+
```
237+
238+
**Examples:**
239+
240+
```bash
241+
# Rotate a Bearer token without touching anything else on the server
242+
curl -X PATCH -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
243+
-d '{"headers":{"Authorization":"Bearer new-token"}}' \
244+
http://127.0.0.1:8080/api/v1/servers/synapbus
245+
246+
# Remove a stale header (the JSON null is the delete signal)
247+
curl -X PATCH -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
248+
-d '{"headers":{"X-Stale":null}}' \
249+
http://127.0.0.1:8080/api/v1/servers/synapbus
250+
251+
# Upsert one env var and delete another in a single round-trip
252+
curl -X PATCH -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
253+
-d '{"env":{"LOG_LEVEL":"debug","OBSOLETE":null}}' \
254+
http://127.0.0.1:8080/api/v1/servers/obsidian-pilot
255+
```
256+
257+
**Notes:**
258+
259+
- Empty string `""` is **set-to-empty**, NOT delete. JSON Merge Patch is
260+
explicit about this — only the JSON `null` token deletes.
261+
- Boolean fields (`enabled`, `quarantined`, `reconnect_on_use`) use
262+
pointer-style semantics: absent = preserve, present = explicit value.
263+
264+
#### POST /api/v1/servers/{name}/config-to-secret
265+
266+
Atomically move a header or env value out of `mcp_config.json` and into the
267+
OS keyring. The backend reads the real value from the loaded config, stores
268+
it in the keyring under `secret_name`, and rewrites the config field with
269+
`${keyring:<secret_name>}`. The client never needs to possess the plaintext
270+
— useful when the API redacts sensitive header values on the read path.
271+
272+
**Request body:**
273+
274+
```json
275+
{
276+
"scope": "header",
277+
"key": "Authorization",
278+
"secret_name": "synapbus-auth"
279+
}
280+
```
281+
282+
| Field | Type | Description |
283+
|---|---|---|
284+
| `scope` | string | `header` or `env` |
285+
| `key` | string | The key on the server's headers / env map |
286+
| `secret_name` | string | Name to store the value under in the OS keyring |
287+
288+
**Response (200 OK):**
289+
290+
```json
291+
{
292+
"success": true,
293+
"data": {
294+
"message": "header \"Authorization\" on \"synapbus\" now references keyring secret \"synapbus-auth\"",
295+
"reference": "${keyring:synapbus-auth}"
296+
}
297+
}
298+
```
299+
300+
**Failure cases:**
301+
302+
| Status | Cause |
303+
|---|---|
304+
| 400 | Missing `scope` / `key` / `secret_name`, invalid scope, value is already a `${keyring:…}` or `${env:…}` reference, or value is empty |
305+
| 404 | Server or key not found |
306+
| 500 | Secret resolver unavailable, keyring store failed, or config update failed |
307+
308+
This endpoint is what the Web UI and macOS tray "Convert to secret" button
309+
calls. It works even for headers the API redacts (the backend has the real
310+
value on disk).
311+
170312
#### POST /api/v1/servers/{name}/enable
171313

172314
Enable a server.

docs/cli/management-commands.md

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,54 @@ mcpproxy upstream enable server-name
8484
mcpproxy upstream disable server-name
8585
```
8686

87+
### Patch Headers / Env
88+
89+
`mcpproxy upstream patch` updates HTTP `headers` and stdio `env` on an
90+
existing server using JSON Merge Patch semantics — keys you specify are
91+
upserted, keys named in `--header-remove` / `--env-remove` are deleted,
92+
and every other key on the stored config is preserved.
93+
94+
This means you can rotate a single Bearer token without seeing or
95+
touching any other header. The same applies to env vars on stdio servers.
96+
97+
```bash
98+
# Rotate the Authorization header on a connected server
99+
mcpproxy upstream patch synapbus --header "Authorization: Bearer new-token"
100+
101+
# Add a custom header without disturbing existing ones
102+
mcpproxy upstream patch synapbus --header "X-Trace: on"
103+
104+
# Remove a stale header
105+
mcpproxy upstream patch synapbus --header-remove "X-Old"
106+
107+
# Set + remove in one round-trip
108+
mcpproxy upstream patch synapbus --header "X-New: v" --header-remove "X-Old"
109+
110+
# Update env vars on a stdio server
111+
mcpproxy upstream patch obsidian-pilot \
112+
--env "LOG_LEVEL=debug" --env-remove "OBSOLETE_VAR"
113+
```
114+
115+
**Flags** (all repeatable):
116+
117+
| Flag | Semantics |
118+
|---|---|
119+
| `--header NAME: value` | Upsert one header (single colon delimits name and value) |
120+
| `--header-remove NAME` | Delete a header by name |
121+
| `--env KEY=value` | Upsert one env var |
122+
| `--env-remove KEY` | Delete an env var by name |
123+
124+
**Notes:**
125+
126+
- Requires the daemon to be running (`mcpproxy serve`). The subcommand
127+
applies changes through the live REST endpoint so connection state
128+
and OAuth tokens stay coordinated; editing `mcp_config.json` by hand
129+
is only safe while the daemon is offline.
130+
- Specifying the same key in both `--header` and `--header-remove` is a
131+
conflict and errors out with a useful message.
132+
- For new servers, use `upstream add` (HTTP/stdio) or
133+
`upstream add-json` (full JSON shape) instead.
134+
87135
## Socket Communication
88136

89137
CLI commands automatically detect and use Unix socket/named pipe communication when the daemon is running.
@@ -95,7 +143,7 @@ CLI commands automatically detect and use Unix socket/named pipe communication w
95143
- No redundant server connection overhead
96144

97145
**Commands with socket support:**
98-
- `upstream list/logs/enable/disable/restart`
146+
- `upstream list/logs/enable/disable/restart/patch`
99147
- `doctor` (requires daemon)
100148
- `call tool`
101149
- `code exec`

docs/configuration/upstream-servers.md

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,120 @@ Servers requiring OAuth 2.1 authentication:
7373
| `env` | object | No | Environment variables to pass |
7474
| `oauth` | object | No | OAuth configuration |
7575

76+
## Headers, Environment Variables, and Secrets
77+
78+
Both HTTP `headers` and stdio `env` are first-class config fields you can
79+
inspect and edit from the Web UI, the macOS tray, the CLI, and the REST
80+
API. The wire format and semantics are identical across surfaces.
81+
82+
### How the API displays them
83+
84+
The REST API (`GET /api/v1/servers`), the SSE `servers.changed` event, and
85+
the MCP `upstream_servers list` tool all redact sensitive header values
86+
by default. The mask format is:
87+
88+
```
89+
••••<last2> (<N> chars)
90+
```
91+
92+
…e.g. a 71-character Bearer token whose last two characters are `59`
93+
appears on the wire as `••••59 (71 chars)`. The mask preserves enough
94+
context to identify which token is in use without leaking the secret.
95+
96+
Values that are already secret **references**`${keyring:NAME}` or
97+
`${env:VAR}` — pass through unchanged: they're labels, not secrets.
98+
99+
This redaction was added in PR #425 to close a real exfiltration path —
100+
a prompt-injected agent calling `upstream_servers list` would otherwise
101+
get back another upstream's Bearer token in plaintext.
102+
103+
To disable redaction (for debugging only), set `reveal_secret_headers: true`
104+
in `mcp_config.json`. **It's not normally needed**: the editing flow
105+
described below works without ever exposing the plaintext to the client.
106+
107+
### How you edit them
108+
109+
#### Web UI / macOS tray
110+
111+
The Server Detail page → Configuration tab has dedicated **Headers** and
112+
**Environment Variables** cards with per-row affordances:
113+
114+
![Headers card on the Web UI](../screenshots/server-detail/web-headers-card.png)
115+
116+
- **Add**`+ Add header` / `+ Add variable` button at the top.
117+
- **Edit** — pencil icon turns the value cell into an input. Save / Cancel.
118+
- **Delete** — trash icon. Confirms first.
119+
- **Convert to secret** — lock icon. Opens a modal asking for a name,
120+
then atomically moves the value into the OS keyring and replaces the
121+
config field with `${keyring:NAME}`.
122+
123+
![Convert-to-secret modal](../screenshots/server-detail/web-convert-modal.png)
124+
125+
The Convert flow works even on masked headers: the backend has the real
126+
value in `mcp_config.json` and never needs the client to send it.
127+
128+
`${keyring:…}` references render as a labeled chip instead of a masked
129+
literal, so converted headers are visually distinct.
130+
131+
#### CLI
132+
133+
```bash
134+
# Upsert one or more headers
135+
mcpproxy upstream patch synapbus --header "X-Trace: on"
136+
137+
# Rotate Authorization in place
138+
mcpproxy upstream patch synapbus --header "Authorization: Bearer new-token"
139+
140+
# Delete one
141+
mcpproxy upstream patch synapbus --header-remove "X-Stale"
142+
143+
# Mix set + delete in a single round-trip
144+
mcpproxy upstream patch synapbus --header "X-New: v" --header-remove "X-Old"
145+
146+
# Env vars (stdio servers)
147+
mcpproxy upstream patch obsidian-pilot --env "LOG_LEVEL=debug"
148+
mcpproxy upstream patch obsidian-pilot --env-remove "OBSOLETE"
149+
```
150+
151+
See [Management Commands](../cli/management-commands.md#patch-headers--env)
152+
for the full flag reference.
153+
154+
#### REST API
155+
156+
`PATCH /api/v1/servers/{name}` follows JSON Merge Patch
157+
([RFC 7396](https://www.rfc-editor.org/rfc/rfc7396)):
158+
159+
```bash
160+
curl -X PATCH -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
161+
-d '{"headers":{"X-New":"value","X-Stale":null}}' \
162+
http://127.0.0.1:8080/api/v1/servers/synapbus
163+
```
164+
165+
- non-null string value → upsert
166+
- JSON `null` value → delete
167+
- key absent from body → preserve (this is how the UI sends a diff
168+
against the masked view without overwriting unchanged values)
169+
170+
See [REST API › PATCH](../api/rest-api.md#patch-apiv1serversname) for the
171+
full reference, including the
172+
[`/config-to-secret`](../api/rest-api.md#post-apiv1serversnameconfig-to-secret)
173+
endpoint that backs the "Convert to secret" affordance.
174+
175+
### Secret references
176+
177+
`headers` and `env` values support two reference shapes that get resolved
178+
at request time rather than stored in plaintext:
179+
180+
| Reference | Source | Lifetime |
181+
|---|---|---|
182+
| `${keyring:NAME}` | OS keyring entry called `NAME` | Persists across restarts; managed via the Secrets page or `mcpproxy secrets` CLI |
183+
| `${env:VAR}` | The `VAR` environment variable on the mcpproxy process | Tied to the shell/launcher that started mcpproxy |
184+
185+
When mcpproxy connects to an upstream it substitutes the reference with
186+
the actual secret. The reference itself never reaches the upstream MCP
187+
server. See [Keyring Integration](../features/keyring-integration.md)
188+
for the full secret-storage story.
189+
76190
## Docker Isolation
77191

78192
For enhanced security, stdio servers can run in Docker containers:
137 KB
Loading
53.4 KB
Loading

0 commit comments

Comments
 (0)